OpenCHAMI / roadmap

Public Roadmap Project for Ochami
MIT License
0 stars 0 forks source link

[RFD] Standardization of JWTs for user authorization #11

Open alexlovelltroy opened 6 months ago

alexlovelltroy commented 6 months ago

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.

  1. API Gateway Validation:

    • The API Gateway will perform the initial validation of the JWT. It will check if the JWT is correctly signed by a known issuer, ensuring the token's integrity and authenticity.
    • It will verify that the token's date claims (like iat, nbf, exp) are within reasonable ranges, preventing expired or not-yet-valid tokens from being used.
    • The API Gateway will also confirm that the first downstream service intended to process the request is included in the JWT's aud (audience) claim.
  2. Service Endpoint Validation:

    • Each service endpoint will perform its own validation of the JWT to ensure that the request has the necessary permissions for the requested operation. This double validation ensures an additional layer of security.

JWT Claim Usage

Consequences

alexlovelltroy commented 6 months ago

I've created a demo repository that includes a separate issuer and verifier in the examples directory.

https://github.com/OpenCHAMI/jwt-authz

alexlovelltroy commented 6 months ago

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.

bcfriesen commented 6 months ago

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?

alexlovelltroy commented 6 months ago

The standard is more important than the library, but yes. I think a library is a good idea.

bcfriesen commented 6 months ago

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?

alexlovelltroy commented 6 months ago

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.

bcfriesen commented 6 months ago

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.

alexlovelltroy commented 6 months ago

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.

miguelgila commented 5 months ago

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.

alexlovelltroy commented 3 weeks ago

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.

https://github.com/OpenCHAMI/node-orchestrator/blob/9d5d661eedb17044d502bc3c40a7aaccb307068a/main.go#L144C1-L173C2

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"}))