Open alexlovelltroy opened 6 months ago
I've created a demo repository that includes a separate issuer and verifier in the examples directory.
This specifically doesn't specify what will create or manage the JWTs. Keycloak and AWS cognito can both create and manage JWTs that conform to the spec. Likely lots of other tools as well.
In the scenario where each micro service is responsible for parsing JWTs, is the idea to reuse a common parser library (or module, or whatever Go calls them) across all OCHAMI micro services to avoid duplicating code?
The standard is more important than the library, but yes. I think a library is a good idea.
Regarding the list of registered claims, those are exactly the list of claims in the JSON Web Token Claims Registry in RFC7519, so if these are expected to be required then I think that is reasonable. (On a related note, would it improve clarity to borrow the MUST/SHOULD/MAY language from RFC2119?)
Regarding the "additional claims," are those expected to be required components of the JWT too?
I am curious how scope
is different from roles
or permissions
. Intuition suggests (to me at least) that they are different words to describe a similar concept, i.e., whether the JWT permits the subject to perform a desired action at the API endpoint. Do you have an example that would distinguish them?
The goal of this RFD is to specify how the claims should be interpreted by the microservices. The IETF RFC describes the registered claims in such general terms that we can't solely rely on it.
As for scope and roles/permissions, I'm suggesting that a role may be analogous to a RBAC group from LDAP and that a scope acts as a limiter in the case that a token is only viable for mutating the state of a subset of the resources behind an API. As an example, we can create a specific API Key for a member of the Admin Group that we want to be able to act with admin permissions, but since we want to embed it in a script that can change the boot image associated with a job, we only want that token to be valid for a subset of nodes that are part of the job.
The goal of this RFD is to specify how the claims should be interpreted by the microservices. The IETF RFC describes the registered claims in such general terms that we can't solely rely on it.
OK. Since there is no normative or informative text in the "JWT Claim Usage" section, it was not clear to me whether you were proposing "Registered Claims" and/or "Additional Claims for Authorization" as required components of a JWT, or if they were just examples. It sounds like it is the latter?
As for scope and roles/permissions, I'm suggesting that a role may be analogous to a RBAC group from LDAP and that a scope acts as a limiter in the case that a token is only viable for mutating the state of a subset of the resources behind an API. As an example, we can create a specific API Key for a member of the Admin Group that we want to be able to act with admin permissions, but since we want to embed it in a script that can change the boot image associated with a job, we only want that token to be valid for a subset of nodes that are part of the job.
That makes sense, thanks.
In discussing multi-tenancy, we're considering adding a claim for partition-id
that is an array of uuids for which the jwt is valid to act. Microservices can use this information to limit the scope of an action to resources within the named partitions.
How would the claim for partition-id
work with hierarchical multi-tenancy? I'm thinking about the use case where a customer has a partition, and has several clusters/scopes within that partition.
🤔 I guess the real question would be, how is multi-tenancy going to be addressed with respect to partitions? This might have a direct impact on how the JWT is designed.
As part of an experimental repo, we created a standard middleware that can validate jwts to ensure they are properly signed and contain a set of required claims.
The idea here is that each microservice may choose to extend the usage of this middleware and use the claims in the business logic.
func AuthenticatorWithRequiredClaims(ja *jwtauth.JWTAuth, requiredClaims []string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token, claims, err := jwtauth.FromContext(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
if token == nil || jwt.Validate(token, ja.ValidateOptions()...) != nil {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
for _, claim := range requiredClaims {
if _, ok := claims[claim]; !ok {
err := fmt.Errorf("missing required claim %s", claim)
log.Error().Err(err).Msg("Missing required claim")
http.Error(w, "missing required claim", http.StatusUnauthorized)
return
}
}
// Token is authenticated and all required claims are present, pass it through
next.ServeHTTP(w, r)
})
}
}
In this example usage, every handler that is part of the chi Group will have the Authentication middleware applied an require that the subject, issuer, and audience are filled in. The Verifier middlware separately ensures that the signature of the jwt is valid and that it is within the timestamp bounds.
// Initialize router
r := chi.NewRouter()
// Protected routes
r.Group(func(r chi.Router) {
// Seek, verify and validate JWT tokens
r.Use(jwtauth.Verifier(tokenAuth))
// Handle valid / invalid tokens.
r.Use(AuthenticatorWithRequiredClaims(tokenAuth, []string{"sub", "iss", "aud"}))
Architectural Decision Record: Implementing JWT-Based Authorization
Context
In microservices architecture, securing API access is a critical concern. We need a reliable, scalable, and efficient way to manage authentication and authorization across different services. This RFD is constrained for discussion of using a bearer token for authorization of a client to one or more microservices. Client authentication is out of scope for this RFD as is service to service communication.
JSON Web Tokens (JWT)s are described in IETF RFC 7519 with best practices in IETF RFC 8725. They provide a standardized way of securely transmitting information about the entity making a request and are widely used in modern web applications.
Decision
We propose a JWT-based authorization scheme, where JWTs serve as bearer tokens in HTTP headers. Each token will must meet the standards below for use of registered claims in addition to reasonable date claims. The token must be signed securely by a known issuer. For defense in depth, the token must be initially validated at any API Gateway and re-validated by each service involved in delivery of the response. This is in addition to any policy processing that may happen within any service mesh.
API Gateway Validation:
iat
,nbf
,exp
) are within reasonable ranges, preventing expired or not-yet-valid tokens from being used.aud
(audience) claim.Service Endpoint Validation:
JWT Claim Usage
Registered Claims:
iss
(Issuer): To identify the issuer of the token. Each authentication scope should have a separate issuersub
(Subject): To identify the principal that is the subject of the JWT. The subject must be unique within the issuer. In any case where an authorization decision cannot be made through data in the token, a service should be able to look up extended permissions information at the issuer based on the subject. (API Key ID)aud
(Audience): To ensure that the JWT is intended for the service or API receiving it. Any microservice not included in the array of strings in theaud
claim should not perform any action based on the authorization in the tokenexp
(Expiration Time): To check token's validity period.nbf
(Not Before): To identify the time before which the JWT must not be accepted for processing.iat
(Issued At): To know when the token was issued.jti
(JWT ID): To provide a unique identifier for the JWT.Additional Claims for Authorization:
roles
/permissions
: To specify the roles or permissions assigned to the token's subject, enabling fine-grained access control.scope
: To define the scope of access granted by the token.Consequences