ory / fosite

Extensible security first OAuth 2.0 and OpenID Connect SDK for Go.
https://www.ory.sh/?utm_source=github&utm_medium=banner&utm_campaign=fosite
Apache License 2.0
2.32k stars 359 forks source link

feat: [#631] JWT Encryption support for client authentication and ID Token generation #764

Open vivshankar opened 1 year ago

vivshankar commented 1 year ago

See #631

Related Issue or Design Document

See #631

Checklist

Further comments

vivshankar commented 1 year ago

@aeneasr Before I go too far down this rabbit hole, I wanted your opinion on replacing the current mechanism for validating JWTs that effectively relies on a KeyFunc returning a key, typically from jwks/jwks_uri config. In the implementation I am introducing here, starting with client authentication, I am expanding support to do the following -

I have a few cases where the approach I am introducing here applies or will apply -

  1. Request object
  2. DCR with SSA validation and DCR client_metadata as application/jose (AU-CDR, for example, uses this)
  3. Token exchange for custom JWT type as a subject or actor token
  4. JWT bearer grant flow (would be an enhancement on top of what is available today)

In all cases, I am trying to preserve the current behavior (unit tests confirm it) while adding this extra option to validate the JWT but it may effectively negate the need for some functions that are in use today. It also introduces decryption as part of the same set of changes.

Note here that I am specifically referring to incoming JWTs. The PR also carries a mechanism to encrypt outgoing JWTs for id_tokens, JARM, userinfo as JWT etc. I know some of these aren't yet in place but this sets the foundation to add those capabilities.

I am, by no means, done with this PR though it is ready for review.

james-d-elliott commented 1 week ago

I believe the following is a spec compliant way to derive symmetric key types:

package example

import (
    "crypto/aes"
    "crypto/sha256"
    "crypto/sha512"
    "fmt"
    "hash"

    "github.com/go-jose/go-jose/v4"
)

func DeriveSymmetricKey(secret []byte, kid, alg, enc, use string) (jwk *jose.JSONWebKey, err error) {
    if len(secret) == 0 {
        return nil, fmt.Errorf("error occurred deriving symmetric jwk: client secret is not configured")
    }

    switch use {
    case "sig":
        return &jose.JSONWebKey{
            Key:       secret,
            KeyID:     kid,
            Algorithm: alg,
            Use:       use,
        }, nil
    case "enc":
        var (
            hasher hash.Hash
            bits   int
        )

        keyAlg := jose.KeyAlgorithm(alg)

        switch keyAlg {
        case jose.A128KW, jose.A128GCMKW, jose.A192KW, jose.A192GCMKW, jose.A256KW, jose.A256GCMKW, jose.PBES2_HS256_A128KW:
            hasher = sha256.New()
        case jose.PBES2_HS384_A192KW:
            hasher = sha512.New384()
        case jose.PBES2_HS512_A256KW, jose.DIRECT:
            hasher = sha512.New()
        default:
            return nil, fmt.Errorf("error occurred deriving symmetric jwk: the encryption key algorithm '%s' is not supported", enc)
        }

        switch keyAlg {
        case jose.A128KW, jose.A128GCMKW, jose.PBES2_HS256_A128KW:
            bits = aes.BlockSize
        case jose.A192KW, jose.A192GCMKW, jose.PBES2_HS384_A192KW:
            bits = aes.BlockSize * 1.5
        case jose.A256KW, jose.A256GCMKW, jose.PBES2_HS512_A256KW:
            bits = aes.BlockSize * 2
        case jose.DIRECT:
            switch jose.ContentEncryption(enc) {
            case jose.A128CBC_HS256, "":
                bits = aes.BlockSize * 2
            case jose.A192CBC_HS384:
                bits = aes.BlockSize * 3
            case jose.A256CBC_HS512:
                bits = aes.BlockSize * 4
            default:
                return nil, fmt.Errorf("error occurred deriving symmetric jwk: the encryption key algortihm '%s' does not support content encryption '%s'", alg, enc)
            }
        }

        if _, err = hasher.Write(secret); err != nil {
            return nil, fmt.Errorf("error occurred deriving symmetric jwk: error occurred hasing the secret: %w", err)
        }

        return &jose.JSONWebKey{
            Key:       hasher.Sum(nil)[:bits],
            KeyID:     kid,
            Algorithm: alg,
            Use:       use,
        }, nil
    default:
        return nil, fmt.Errorf("error occurred deriving symmetric jwk: the use '%s' is not supported", use)
    }
}