diff --git a/oauth2/handler.go b/oauth2/handler.go index 54c6573038..dd4e7f52f3 100644 --- a/oauth2/handler.go +++ b/oauth2/handler.go @@ -20,6 +20,7 @@ import ( "github.com/ory/hydra/v2/x/events" "github.com/ory/x/httprouterx" "github.com/ory/x/josex" + "github.com/ory/x/stringsx" jwtV5 "github.com/golang-jwt/jwt/v5" @@ -1332,14 +1333,32 @@ func (h *Handler) createVerifiableCredential(w http.ResponseWriter, r *http.Requ return } - session.Claims.Add("vc", map[string]any{ - "@context": []string{"https://www.w3.org/2018/credentials/v1"}, - "type": []string{"VerifiableCredential", "UserInfoCredential"}, - "credentialSubject": map[string]any{ - // Encode ID according to https://github.com/quartzjer/did-jwk/blob/main/spec.md - "id": fmt.Sprintf("did:jwk:%s", base64.RawURLEncoding.EncodeToString(proofJWKJSON)), + // Encode ID according to https://github.com/quartzjer/did-jwk/blob/main/spec.md + vcID := fmt.Sprintf("did:jwk:%s", base64.RawURLEncoding.EncodeToString(proofJWKJSON)) + vcClaims := &VerifableCredentialClaims{ + RegisteredClaims: jwtV5.RegisteredClaims{ + Issuer: session.Claims.Issuer, + ID: stringsx.Coalesce(session.Claims.JTI, uuid.New()), + IssuedAt: jwtV5.NewNumericDate(session.Claims.IssuedAt), + NotBefore: jwtV5.NewNumericDate(session.Claims.IssuedAt), + ExpiresAt: jwtV5.NewNumericDate(session.Claims.IssuedAt.Add(1 * time.Hour)), + Subject: vcID, }, - }) + VerifiableCredential: VerifiableCredentialClaim{ + Context: []string{"https://www.w3.org/2018/credentials/v1"}, + Type: []string{"VerifiableCredential", "UserInfoCredential"}, + Subject: map[string]any{ + "id": vcID, + "sub": session.Claims.Subject, + }, + }, + } + if session.Claims.Extra != nil { + for claim, val := range session.Claims.Extra { + vcClaims.VerifiableCredential.Subject[claim] = val + } + } + signingKeyID, err := h.r.OpenIDJWTStrategy().GetPublicKeyID(ctx) if err != nil { h.r.Writer().WriteError(w, r, errorsx.WithStack(err)) @@ -1347,7 +1366,12 @@ func (h *Handler) createVerifiableCredential(w http.ResponseWriter, r *http.Requ } headers := jwt.NewHeaders() headers.Add("kid", signingKeyID) - rawToken, _, err := h.r.OpenIDJWTStrategy().Generate(ctx, session.Claims.ToMapClaims(), headers) + mapClaims, err := vcClaims.ToMapClaims() + if err != nil { + h.r.Writer().WriteError(w, r, errorsx.WithStack(err)) + return + } + rawToken, _, err := h.r.OpenIDJWTStrategy().Generate(ctx, mapClaims, headers) if err != nil { h.r.Writer().WriteError(w, r, errorsx.WithStack(err)) return @@ -1356,49 +1380,3 @@ func (h *Handler) createVerifiableCredential(w http.ResponseWriter, r *http.Requ response.Credential = rawToken h.r.Writer().Write(w, r, &response) } - -// Request a Verifiable Credential -// -// swagger:parameters createVerifiableCredential -// -//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions -type createVerifiableCredentialRequest struct { - // in: body - Body CreateVerifiableCredentialRequestBody -} - -// CreateVerifiableCredentialRequestBody contains the request body to request a verifiable credential. -// -// swagger:parameters createVerifiableCredentialRequestBody -type CreateVerifiableCredentialRequestBody struct { - Format string `json:"format"` - Types []string `json:"types"` - Proof *VerifiableCredentialProof `json:"proof"` -} - -// VerifiableCredentialProof contains the proof of a verifiable credential. -// -// swagger:parameters verifiableCredentialProof -type VerifiableCredentialProof struct { - ProofType string `json:"proof_type"` - JWT string `json:"jwt"` -} - -// VerifiableCredentialResponse contains the verifiable credential. -// -// swagger:model verifiableCredentialResponse -type VerifiableCredentialResponse struct { - Format string `json:"format"` - Credential string `json:"credential_draft_00"` -} - -// VerifiableCredentialPrimingResponse contains the nonce to include in the proof-of-possession JWT. -// -// swagger:model verifiableCredentialPrimingResponse -type VerifiableCredentialPrimingResponse struct { - Format string `json:"format"` - Nonce string `json:"c_nonce"` - NonceExpiresIn int64 `json:"c_nonce_expires_in"` - - fosite.RFC6749ErrorJson -} diff --git a/oauth2/oauth2_auth_code_test.go b/oauth2/oauth2_auth_code_test.go index e94eecba9d..3d1c68273a 100644 --- a/oauth2/oauth2_auth_code_test.go +++ b/oauth2/oauth2_auth_code_test.go @@ -371,7 +371,7 @@ func TestAuthCodeWithDefaultStrategy(t *testing.T) { GrantAccessTokenAudience: rr.RequestedAccessTokenAudience, Session: &hydra.AcceptOAuth2ConsentRequestSession{ AccessToken: map[string]interface{}{"foo": "bar"}, - IdToken: map[string]interface{}{"bar": "baz"}, + IdToken: map[string]interface{}{"email": "foo@bar.com", "bar": "baz"}, }, }). Execute() @@ -1159,12 +1159,15 @@ func assertCreateVerifiableCredential(t *testing.T, reg driver.Registry, nonce s }, }) require.NoError(t, err) - assertVerifiableCredentialContainsPublicKey(t, reg, verifiableCredential, pubKeyJWK) + require.NotNil(t, verifiableCredential) + + _, claims := claimsFromVCResponse(t, reg, verifiableCredential) + assertClaimsContainPublicKey(t, claims, pubKeyJWK) } -func assertVerifiableCredentialContainsPublicKey(t *testing.T, reg driver.Registry, vc *hydraoauth2.VerifiableCredentialResponse, pubKeyJWK *jose.JSONWebKey) { +func claimsFromVCResponse(t *testing.T, reg driver.Registry, vc *hydraoauth2.VerifiableCredentialResponse) (*jwt.Token, *hydraoauth2.VerifableCredentialClaims) { ctx := context.Background() - token, err := jwt.Parse(vc.Credential, func(token *jwt.Token) (interface{}, error) { + token, err := jwt.ParseWithClaims(vc.Credential, new(hydraoauth2.VerifableCredentialClaims), func(token *jwt.Token) (interface{}, error) { kid, found := token.Header["kid"] if !found { return nil, errors.New("missing kid header") @@ -1180,10 +1183,15 @@ func assertVerifiableCredentialContainsPublicKey(t *testing.T, reg driver.Regist return x.Must(reg.OpenIDJWTStrategy().GetPublicKey(ctx)).Key, nil }) require.NoError(t, err) + + return token, token.Claims.(*hydraoauth2.VerifableCredentialClaims) +} + +func assertClaimsContainPublicKey(t *testing.T, claims *hydraoauth2.VerifableCredentialClaims, pubKeyJWK *jose.JSONWebKey) { pubKeyRaw, err := pubKeyJWK.MarshalJSON() require.NoError(t, err) expectedID := fmt.Sprintf("did:jwk:%s", base64.RawURLEncoding.EncodeToString(pubKeyRaw)) - require.Equal(t, expectedID, token.Claims.(jwt.MapClaims)["vc"].(map[string]any)["credentialSubject"].(map[string]any)["id"]) + require.Equal(t, expectedID, claims.VerifiableCredential.Subject["id"]) } func createVerifiableCredential( diff --git a/oauth2/verifiable_credentials.go b/oauth2/verifiable_credentials.go new file mode 100644 index 0000000000..3af47e0922 --- /dev/null +++ b/oauth2/verifiable_credentials.go @@ -0,0 +1,87 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oauth2 + +import ( + "encoding/json" + + "github.com/golang-jwt/jwt/v5" + + "github.com/ory/fosite" +) + +// Request a Verifiable Credential +// +// swagger:parameters createVerifiableCredential +// +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type createVerifiableCredentialRequest struct { + // in: body + Body CreateVerifiableCredentialRequestBody +} + +// CreateVerifiableCredentialRequestBody contains the request body to request a verifiable credential. +// +// swagger:parameters createVerifiableCredentialRequestBody +type CreateVerifiableCredentialRequestBody struct { + Format string `json:"format"` + Types []string `json:"types"` + Proof *VerifiableCredentialProof `json:"proof"` +} + +// VerifiableCredentialProof contains the proof of a verifiable credential. +// +// swagger:parameters verifiableCredentialProof +type VerifiableCredentialProof struct { + ProofType string `json:"proof_type"` + JWT string `json:"jwt"` +} + +// VerifiableCredentialResponse contains the verifiable credential. +// +// swagger:model verifiableCredentialResponse +type VerifiableCredentialResponse struct { + Format string `json:"format"` + Credential string `json:"credential_draft_00"` +} + +// VerifiableCredentialPrimingResponse contains the nonce to include in the proof-of-possession JWT. +// +// swagger:model verifiableCredentialPrimingResponse +type VerifiableCredentialPrimingResponse struct { + Format string `json:"format"` + Nonce string `json:"c_nonce"` + NonceExpiresIn int64 `json:"c_nonce_expires_in"` + + fosite.RFC6749ErrorJson +} + +type VerifableCredentialClaims struct { + jwt.RegisteredClaims + VerifiableCredential VerifiableCredentialClaim `json:"vc"` +} +type VerifiableCredentialClaim struct { + Context []string `json:"@context"` + Subject map[string]any `json:"credentialSubject"` + Type []string `json:"type"` +} + +func (v *VerifableCredentialClaims) GetAudience() (jwt.ClaimStrings, error) { + return jwt.ClaimStrings{}, nil +} + +func (v *VerifableCredentialClaims) ToMapClaims() (res map[string]any, err error) { + res = map[string]any{} + + bs, err := json.Marshal(v) + if err != nil { + return nil, err + } + err = json.Unmarshal(bs, &res) + if err != nil { + return nil, err + } + + return res, nil +}