March 1, 2024 in Development, LANL by David J. Allen (LANL)8 minutes
OpenCHAMI uses signed JWTs for authentication and authorization. Users must include a valid token with every request which will then be passed on to every subsequent microservice involved in processing that request. However, there are some internal requests that aren't triggered directly by a user. For these, we still need a valid token, but without a specific user to tie it to, we need to use a different kind of JWT.
OpenCHAMI is a loose collection of microservices that all obey the same rules for interoperability. One important rule is that every request must be positively authenticated. We have chosen bearer token authentication for each request as our preferred authentication method. As covered in our roadmap issue #11, we have selected JSON Web Tokens(JWTs) as our token of choice. It is a signed token that the caller can send in an HTTP header contains enough information to authenticate the user and describe how the token can be used. Users follow a standard Oauth2 authorization flow to obtain their token. Each microservice can then read the token to make decisions about what is permitted in the context of the request. It is common for one microservice to get some information from another microservice in order to fulfil a request. In that case, the user’s original JWT can be forwarded along for other services to work with. Following this pattern, it doesn’t matter how many microservices are involved, the user’s token can be reused in every context without changes.
However, there are some situations in which the user isn’t the originator of the request. For example, when a compute node requests a boot script from bss, there is no “user” involved. When bss makes a call to smd to get more context , we need a way to indicate to smd that it should process the request. We also need a way to record who made the request and why. The Oauth standard has a path we can take. BSS can request its own token through the client credentials grant flow. In this post, we’ll explore how the OpenCHAMI project has extended JWT-based authentication to support client credentials in addition to user-based authentication.
The OAuth 2.0 framework specification, RFC 6749, details how an OAuth client may request a token from an authentication server. This is achieved by performing a client credentials grant flow, similar to how it was described in a previous post, but now we want to implement it directly into the microservice. Of course for this flow, we assume that the clients are trusted and that we have access to the authentication server’s admin endpoints. Like before, Ory Hydra will be used as our authentication server, but we will use the HTTP RESTful API instead of the Hydra CLI tool.
Implementing the flow requires to receive a token only requires three simple steps:
Create an OAuth2 client and make a POST
request to the /admin/clients
Authorize the OAuth2 client with another POST
request to the /oauth2/auth
Receive an access token with a final POST
request to the /oauth2/token
The implementation is done in Go and integrated into our OpenCHAMI fork of BSS.
We need to be able to make HTTP requests to our authentication server to complete each step listed above. To simply things a bit, we can wrap some of the relevant OAuth2 details in a HTTP client.
These are all common parameters need for our requests either in the request’s body or header. We then implement the CreateOAuthClient
function.
The trickest part of making this request is knowing what to include in each request body and header. Most of the request body parameters can be hard coded, but make sure that the body includes redirect_uris
with an “s” and to set any other parameters you’d want the client to have such as scope. The response should then include the client ID, secret, and registeration access token that is needed for the other two requests, which is stored using the OAuthClient
struct declared earlier. (Note: If you included a client ID and secret in the request body, it will be used instead of a generated one.)
The next piece of the puzzle is to authorize the OAuth2 client to receive a token. This step will not work if you don’t set oidc.dynamic_client_registration.enabled=true
in the Hydra config. Also, regardless of the token_endpoint_auth_method
, you are required to include the registration_access_token
in the authorization header when using dynamic registration.
Like in the previous step, this step includes making another request to the authentication server. We include the client ID and secret in the request body, but set the Authorization
header to Bearer {registrationAccessToken}
. Make sure to also include the correct URL encoded redirect_uri
(without an “s”) string, set the same state
parameter as before, and to set the Content-Type
header to application/x-www-form-urlencoded
.
The response doesn’t return anything all that interesting, and as long as you don’t received an error (likely caused by a typo or incorrect request parameter), everything should work fine.
Finally, the last step includes making a request for the access token from the authentication server. Again, we use our client ID, secret, and registration access token to perform the request with similar headers as before. The scope can only be set to one used to create the client in the first step.
After the response is received, the access token has to be extracted from the JSON and then cast to a string. We should now be able to return this token to make requests to other microservices.
After implementing the entire flow, all we need to do now is call the functions and set a variable. Don’t forget to set the endpoint URL strings to point to the appropriate endpoints.
Running this should print the access token if everything worked correctly.
And that’s all there is to it! There’s still some cleaning up to do and improvements to be made to the code presented above, but this gets the job done. Keep in mind that it should not be necessary to have to create and authorize another client to receive another token after each one expires unless you unregister it. Also, it may be a good idea to control when the microservice is requesting a new token such as before making a request and receiving a 401 response. Anyways, these are just some of the considerations to think about and maybe cover in a future post.
If you’re interested in using cloud-like design patterns for the next generation of HPC System Management, we’d love to hear from you. You can reach us through the Contact Us page or through our public Slack instance.