Skip to content

Commit

Permalink
fix: verifiable credentials JWT format (#3614)
Browse files Browse the repository at this point in the history
  • Loading branch information
hperl committed Aug 23, 2023
1 parent 800ce0a commit 0176adc
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 59 deletions.
86 changes: 32 additions & 54 deletions oauth2/handler.go
Expand Up @@ -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"

Expand Down Expand Up @@ -1332,22 +1333,45 @@ 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))
return
}
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
Expand All @@ -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
}
18 changes: 13 additions & 5 deletions oauth2/oauth2_auth_code_test.go
Expand Up @@ -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()
Expand Down Expand Up @@ -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")
Expand All @@ -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(
Expand Down
87 changes: 87 additions & 0 deletions 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
}

0 comments on commit 0176adc

Please sign in to comment.