Skip to content

Commit

Permalink
return metdata objects over raw endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
gerardsn committed May 23, 2024
1 parent b5c1513 commit 55ea77c
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 73 deletions.
85 changes: 43 additions & 42 deletions auth/api/iam/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -720,14 +720,19 @@ func (r Wrapper) RequestOid4vciCredentialIssuance(ctx context.Context, request R
if err != nil {
return nil, core.InvalidInputError("could not parse Issuer DID: %s: %w", request.Body.Issuer, err)
}
// Fetch the endpoints
authorizationEndpoint, tokenEndpoint, credentialEndpoint, err := r.openid4vciEndpoints(ctx, *issuerDid)
// Fetch metadata containing the endpoints
credentialIssuerMetadata, authzServerMetadata, err := r.openid4vciMetadata(ctx, *issuerDid)
if err != nil {
return nil, core.Error(http.StatusFailedDependency, "cannot locate endpoints for %s: %w", issuerDid.String(), err)
}
endpoint, err := url.Parse(authorizationEndpoint)
if err != nil {
return nil, fmt.Errorf("failed to parse the authorization_endpoint: %w", err)
if len(credentialIssuerMetadata.CredentialEndpoint) == 0 {
return nil, errors.New("no credential_endpoint found")
}
if len(authzServerMetadata.AuthorizationEndpoint) == 0 {
return nil, errors.New("no authorization_endpoint found")
}
if len(authzServerMetadata.TokenEndpoint) == 0 {
return nil, errors.New("no token_endpoint found")
}
// Read and parse the authorization details
authorizationDetails := []byte("[]")
Expand All @@ -749,19 +754,25 @@ func (r Wrapper) RequestOid4vciCredentialIssuance(ctx context.Context, request R
}
// Store the session
err = r.openid4vciSessionStore().Put(state, &Oid4vciSession{
HolderDid: requestHolder,
IssuerDid: issuerDid,
RemoteRedirectUri: request.Body.RedirectUri,
RedirectUri: redirectUri.String(),
PKCEParams: pkceParams,
IssuerTokenEndpoint: tokenEndpoint,
IssuerCredentialEndpoint: credentialEndpoint,
HolderDid: requestHolder,
IssuerDid: issuerDid,
RemoteRedirectUri: request.Body.RedirectUri,
RedirectUri: redirectUri.String(),
PKCEParams: pkceParams,
// OpenID4VCI issuers may use multiple Authorization Servers
// We must use the token_endpoint that corresponds to the same Authorization Server used for the authorization_endpoint
IssuerTokenEndpoint: authzServerMetadata.TokenEndpoint,
IssuerCredentialEndpoint: credentialIssuerMetadata.CredentialEndpoint,
})
if err != nil {
return nil, fmt.Errorf("failed to store session: %w", err)
}
// Build the redirect URL, the client browser should be redirected to.
redirectUrl := nutsHttp.AddQueryParams(*endpoint, map[string]string{
authorizationEndpoint, err := url.Parse(authzServerMetadata.AuthorizationEndpoint)
if err != nil {
return nil, fmt.Errorf("failed to parse the authorization_endpoint: %w", err)
}
redirectUrl := nutsHttp.AddQueryParams(*authorizationEndpoint, map[string]string{
oauth.ResponseTypeParam: oauth.CodeResponseType,
oauth.StateParam: state,
oauth.ClientIDParam: requestHolder.String(),
Expand Down Expand Up @@ -835,46 +846,36 @@ func (r Wrapper) CallbackOid4vciCredentialIssuance(ctx context.Context, request
}, nil
}

func (r Wrapper) openid4vciEndpoints(ctx context.Context, issuerDid did.DID) (authorization, token, credential string, err error) {
func (r Wrapper) openid4vciMetadata(ctx context.Context, issuerDid did.DID) (*oauth.OpenIDCredentialIssuerMetadata, *oauth.AuthorizationServerMetadata, error) {
oauthIssuer, err := didweb.DIDToURL(issuerDid)
if err != nil {
return "", "", "", fmt.Errorf("invalid issuer: %w", err)
return nil, nil, fmt.Errorf("invalid issuer: %w", err)
}
credentialIssuerMetadata, err := r.auth.IAMClient().OpenIdCredentialIssuerMetadata(ctx, oauthIssuer.String())
if err != nil {
return "", "", "", err
return nil, nil, err
}

// OpenID4VCI allows multiple AuthorizationServers in credentialIssuerMetadata for a single issuer. (allows delegating issuance per VC type)
// TODO: smart select the correct authorization server based on the metadata
// https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-issuer-metadata-p
// For now we just accept the first successful result, and lookup the metadata.
var ASMetadata *oauth.AuthorizationServerMetadata
if len(credentialIssuerMetadata.AuthorizationServers) == 0 {
// This is an optional field. When no authorization servers are listed, the oauth Issuer is the authorization server
ASMetadata, err = r.auth.IAMClient().AuthorizationServerMetadata(ctx, oauthIssuer.String())
if err != nil {
return "", "", "", err
}
} else {
// TODO: smart select the correct authorization server based on the metadata
// https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-issuer-metadata-p
// For now just accept the first successful result
for _, serverURL := range credentialIssuerMetadata.AuthorizationServers {
ASMetadata, err = r.auth.IAMClient().AuthorizationServerMetadata(ctx, serverURL)
if err == nil {
break
}
for _, serverURL := range credentialIssuerMetadata.AuthorizationServers {
ASMetadata, err = r.auth.IAMClient().AuthorizationServerMetadata(ctx, serverURL)
if err == nil {
break
}
}
if ASMetadata == nil {
// authorization_servers is an optional field. When no authorization servers are listed, the oauth Issuer is the authorization server.
// also try issuer in case all others fail
ASMetadata, err = r.auth.IAMClient().AuthorizationServerMetadata(ctx, oauthIssuer.String())
if err != nil {
return "", "", "", err
return nil, nil, err
}
}
if len(credentialIssuerMetadata.CredentialEndpoint) == 0 {
return "", "", "", errors.New("no credential_endpoint found")
}
if len(ASMetadata.AuthorizationEndpoint) == 0 {
return "", "", "", errors.New("no authorization_endpoint found")
}
if len(ASMetadata.TokenEndpoint) == 0 {
return "", "", "", errors.New("no token_endpoint found")
}
return ASMetadata.AuthorizationEndpoint, ASMetadata.TokenEndpoint, credentialIssuerMetadata.CredentialEndpoint, nil
return credentialIssuerMetadata, ASMetadata, nil
}

// createAuthorizationRequest creates an OAuth2.0 authorizationRequest redirect URL that redirects to the authorization server.
Expand Down
63 changes: 32 additions & 31 deletions auth/api/iam/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1280,7 +1280,7 @@ func TestWrapper_RequestOid4vciCredentialIssuance(t *testing.T) {
assert.Equal(t, `[{"format":"vc+sd-jwt","type":"openid_credential"}]`, redirectUri.Query().Get("authorization_details"))
println(redirectUri.String())
})
t.Run("openid4vciEndpoints", func(t *testing.T) {
t.Run("openid4vciMetadata", func(t *testing.T) {
t.Run("ok - fallback to issuerDID on empty AuthorizationServers", func(t *testing.T) {
ctx := newTestClient(t)
metadata := oauth.OpenIDCredentialIssuerMetadata{
Expand All @@ -1301,6 +1301,7 @@ func TestWrapper_RequestOid4vciCredentialIssuance(t *testing.T) {
ctx.vdr.EXPECT().IsOwner(nil, holderDID).Return(true, nil)
ctx.iamClient.EXPECT().OpenIdCredentialIssuerMetadata(nil, issuerURL).Return(&metadata, nil)
ctx.iamClient.EXPECT().AuthorizationServerMetadata(nil, authServer).Return(nil, assert.AnError)
ctx.iamClient.EXPECT().AuthorizationServerMetadata(nil, issuerURL).Return(nil, assert.AnError)

_, err := ctx.client.RequestOid4vciCredentialIssuance(nil, requestCredentials(holderDID, issuerDID, redirectURI))
assert.ErrorIs(t, err, assert.AnError)
Expand All @@ -1319,36 +1320,6 @@ func TestWrapper_RequestOid4vciCredentialIssuance(t *testing.T) {
_, err := ctx.client.RequestOid4vciCredentialIssuance(nil, requestCredentials(holderDID, issuerDID, redirectURI))
assert.ErrorIs(t, err, assert.AnError)
})
t.Run("error - missing credential_endpoint", func(t *testing.T) {
ctx := newTestClient(t)
ctx.vdr.EXPECT().IsOwner(nil, holderDID).Return(true, nil)
metadata := metadata
metadata.CredentialEndpoint = ""
ctx.iamClient.EXPECT().OpenIdCredentialIssuerMetadata(nil, issuerURL).Return(&metadata, nil)
ctx.iamClient.EXPECT().AuthorizationServerMetadata(nil, authServer).Return(&authzMetadata, nil)
_, err := ctx.client.RequestOid4vciCredentialIssuance(nil, requestCredentials(holderDID, issuerDID, redirectURI))
assert.EqualError(t, err, "cannot locate endpoints for "+issuerDID.String()+": no credential_endpoint found")
})
t.Run("error - missing authorization_endpoint", func(t *testing.T) {
ctx := newTestClient(t)
ctx.vdr.EXPECT().IsOwner(nil, holderDID).Return(true, nil)
authzMetadata := authzMetadata
authzMetadata.AuthorizationEndpoint = ""
ctx.iamClient.EXPECT().OpenIdCredentialIssuerMetadata(nil, issuerURL).Return(&metadata, nil)
ctx.iamClient.EXPECT().AuthorizationServerMetadata(nil, authServer).Return(&authzMetadata, nil)
_, err := ctx.client.RequestOid4vciCredentialIssuance(nil, requestCredentials(holderDID, issuerDID, redirectURI))
assert.EqualError(t, err, "cannot locate endpoints for "+issuerDID.String()+": no authorization_endpoint found")
})
t.Run("error - missing token_endpoint", func(t *testing.T) {
ctx := newTestClient(t)
ctx.vdr.EXPECT().IsOwner(nil, holderDID).Return(true, nil)
authzMetadata := authzMetadata
authzMetadata.TokenEndpoint = ""
ctx.iamClient.EXPECT().OpenIdCredentialIssuerMetadata(nil, issuerURL).Return(&metadata, nil)
ctx.iamClient.EXPECT().AuthorizationServerMetadata(nil, authServer).Return(&authzMetadata, nil)
_, err := ctx.client.RequestOid4vciCredentialIssuance(nil, requestCredentials(holderDID, issuerDID, redirectURI))
assert.EqualError(t, err, "cannot locate endpoints for "+issuerDID.String()+": no token_endpoint found")
})
})
t.Run("error - issuer not a did", func(t *testing.T) {
req := requestCredentials(holderDID, issuerDID, redirectURI)
Expand Down Expand Up @@ -1396,6 +1367,36 @@ func TestWrapper_RequestOid4vciCredentialIssuance(t *testing.T) {

assert.EqualError(t, err, "failed to parse the authorization_endpoint: parse \":\": missing protocol scheme")
})
t.Run("error - missing credential_endpoint", func(t *testing.T) {
ctx := newTestClient(t)
ctx.vdr.EXPECT().IsOwner(nil, holderDID).Return(true, nil)
metadata := metadata
metadata.CredentialEndpoint = ""
ctx.iamClient.EXPECT().OpenIdCredentialIssuerMetadata(nil, issuerURL).Return(&metadata, nil)
ctx.iamClient.EXPECT().AuthorizationServerMetadata(nil, authServer).Return(&authzMetadata, nil)
_, err := ctx.client.RequestOid4vciCredentialIssuance(nil, requestCredentials(holderDID, issuerDID, redirectURI))
assert.EqualError(t, err, "no credential_endpoint found")
})
t.Run("error - missing authorization_endpoint", func(t *testing.T) {
ctx := newTestClient(t)
ctx.vdr.EXPECT().IsOwner(nil, holderDID).Return(true, nil)
authzMetadata := authzMetadata
authzMetadata.AuthorizationEndpoint = ""
ctx.iamClient.EXPECT().OpenIdCredentialIssuerMetadata(nil, issuerURL).Return(&metadata, nil)
ctx.iamClient.EXPECT().AuthorizationServerMetadata(nil, authServer).Return(&authzMetadata, nil)
_, err := ctx.client.RequestOid4vciCredentialIssuance(nil, requestCredentials(holderDID, issuerDID, redirectURI))
assert.EqualError(t, err, "no authorization_endpoint found")
})
t.Run("error - missing token_endpoint", func(t *testing.T) {
ctx := newTestClient(t)
ctx.vdr.EXPECT().IsOwner(nil, holderDID).Return(true, nil)
authzMetadata := authzMetadata
authzMetadata.TokenEndpoint = ""
ctx.iamClient.EXPECT().OpenIdCredentialIssuerMetadata(nil, issuerURL).Return(&metadata, nil)
ctx.iamClient.EXPECT().AuthorizationServerMetadata(nil, authServer).Return(&authzMetadata, nil)
_, err := ctx.client.RequestOid4vciCredentialIssuance(nil, requestCredentials(holderDID, issuerDID, redirectURI))
assert.EqualError(t, err, "no token_endpoint found")
})
}

func requestCredentials(holderDID did.DID, issuerDID did.DID, redirectURI string) RequestOid4vciCredentialIssuanceRequestObject {
Expand Down
2 changes: 2 additions & 0 deletions auth/api/iam/openid4vp.go
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,8 @@ func (r Wrapper) handleCallback(ctx context.Context, request CallbackRequestObje
}, nil
}

// oauthNonceStore is used to map nonce to state. Burn on use.
// This mapping is needed because we have one OAuthSession (state), but use a new nonce for every OpenID4VP flow.
func (r Wrapper) oauthNonceStore() storage.SessionStore {
return r.storageEngine.GetSessionDatabase().GetStore(oAuthFlowTimeout, oauthNonceKey...)
}
Expand Down

0 comments on commit 55ea77c

Please sign in to comment.