Skip to content

Commit

Permalink
feat: 5.1.2. Using scope Parameter to Request Issuance of a Credential (
Browse files Browse the repository at this point in the history
trustbloc#1595)

Signed-off-by: Mykhailo Sizov <mykhailo.sizov@securekey.com>
  • Loading branch information
mishasizov-SK committed Feb 16, 2024
1 parent cc25efd commit 660207c
Show file tree
Hide file tree
Showing 17 changed files with 850 additions and 1,079 deletions.
86 changes: 64 additions & 22 deletions component/wallet-cli/pkg/oidc4vci/oidc4vci_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,8 @@ func (f *Flow) getAuthorizationCode(oauthClient *oauth2.Config, issuerState stri
slog.Info("Getting authorization code",
"client_id", oauthClient.ClientID,
"scopes", oauthClient.Scopes,
"credential_configuration_id", f.credentialConfigurationID,
"format", f.oidcCredentialFormat,
"redirect_uri", oauthClient.RedirectURL,
"authorization_endpoint", oauthClient.Endpoint.AuthURL,
)
Expand Down Expand Up @@ -472,17 +474,24 @@ func (f *Flow) getAuthorizationCode(oauthClient *oauth2.Config, issuerState stri

oauthClient.RedirectURL = redirectURI.String()

authCodeOptions := []oauth2.AuthCodeOption{
oauth2.SetAuthURLParam("issuer_state", issuerState),
oauth2.SetAuthURLParam("code_challenge", "MLSjJIlPzeRQoN9YiIsSzziqEuBSmS4kDgI3NDjbfF8"),
oauth2.SetAuthURLParam("code_challenge_method", "S256"),
}

authorizationDetailsRequestBody, err := f.getAuthorizationDetailsRequestBody(
f.credentialType, f.credentialConfigurationID, f.oidcCredentialFormat)
if err != nil {
return "", fmt.Errorf("getAuthorizationDetailsRequestBody: %w", err)
}

authCodeOptions := []oauth2.AuthCodeOption{
oauth2.SetAuthURLParam("issuer_state", issuerState),
oauth2.SetAuthURLParam("code_challenge", "MLSjJIlPzeRQoN9YiIsSzziqEuBSmS4kDgI3NDjbfF8"),
oauth2.SetAuthURLParam("code_challenge_method", "S256"),
oauth2.SetAuthURLParam("authorization_details", string(authorizationDetailsRequestBody)),
// If neither credential_configuration_id nor format params supplied authorizationDetailsRequestBody will be empty.
// In this case Wallet CLI should use scope parameter to request credential type:
// Spec: https://openid.github.io/OpenID4VCI/openid-4-verifiable-credential-issuance-wg-draft.html#section-5.1.2
if len(authorizationDetailsRequestBody) > 0 {
authCodeOptions = append(authCodeOptions,
oauth2.SetAuthURLParam("authorization_details", string(authorizationDetailsRequestBody)))
}

if f.enableDiscoverableClientID {
Expand Down Expand Up @@ -720,22 +729,9 @@ func (f *Flow) receiveVC(
return nil, fmt.Errorf("build proof: %w", err)
}

// Take default value as f.oidcCredentialFormat
oidcCredentialFormat := f.oidcCredentialFormat

if f.credentialConfigurationID != "" {
// For cases, when oidcCredentialFormat is not supplied, but credentialConfigurationID option available,
// we can take format from well-known configuration.
// Spec: https://openid.github.io/OpenID4VCI/openid-4-verifiable-credential-issuance-wg-draft.html#section-5.1.1
format := wellKnown.CredentialConfigurationsSupported.AdditionalProperties[f.credentialConfigurationID].Format
if format == "" {
return nil, fmt.Errorf(
"unable to obtain OIDC credential format from issuer well-known configuration. "+
"Check if `issuer.credentialMetadata.credential_configurations_supported` contains key `%s` "+
"with nested `format` field", f.credentialConfigurationID)
}

oidcCredentialFormat = vcsverifiable.OIDCFormat(format)
oidcCredentialFormat, err := f.getCredentialRequestOIDCCredentialFormat(wellKnown)
if err != nil {
return nil, fmt.Errorf("getCredentialRequestOIDCCredentialFormat: %w", err)
}

b, err := json.Marshal(CredentialRequest{
Expand Down Expand Up @@ -837,6 +833,48 @@ func (f *Flow) receiveVC(
return parsedVC, nil
}

func (f *Flow) getCredentialRequestOIDCCredentialFormat(
wellKnown *issuerv1.WellKnownOpenIDIssuerConfiguration,
) (vcsverifiable.OIDCFormat, error) {
// Take default value as f.oidcCredentialFormat
if f.oidcCredentialFormat != "" {
return f.oidcCredentialFormat, nil
}

// For cases, when oidcCredentialFormat is not supplied:
if f.credentialConfigurationID != "" {
// CredentialConfigurationID option available so take format from well-known configuration.
// Spec: https://openid.github.io/OpenID4VCI/openid-4-verifiable-credential-issuance-wg-draft.html#section-5.1.1
format := wellKnown.CredentialConfigurationsSupported.AdditionalProperties[f.credentialConfigurationID].Format
if format == "" {
return "", fmt.Errorf(
"unable to obtain OIDC credential format from issuer well-known configuration. "+
"Check if `issuer.credentialMetadata.credential_configurations_supported` contains key `%s` "+
"with nested `format` field", f.credentialConfigurationID)
}

return vcsverifiable.OIDCFormat(format), nil
}

if len(f.scopes) > 0 {
// scopes option available so take format from well-known configuration.
// Spec: https://openid.github.io/OpenID4VCI/openid-4-verifiable-credential-issuance-wg-draft.html#section-5.1.2
for _, scope := range f.scopes {
for _, credentialConfiguration := range wellKnown.CredentialConfigurationsSupported.AdditionalProperties {
if lo.FromPtr(credentialConfiguration.Scope) == scope {
return vcsverifiable.OIDCFormat(credentialConfiguration.Format), nil
}
}
}

return "", fmt.Errorf(
"unable to obtain OIDC credential format from issuer well-known configuration. "+
"Check if `issuer.credentialMetadata.credential_configurations_supported` contains nested object "+
"with `scope` field equals to one of the %v", f.scopes)
}

return "", errors.New("obtain OIDC credential format")
}
func (f *Flow) handleIssuanceAck(
wellKnown *issuerv1.WellKnownOpenIDIssuerConfiguration,
credResponse *CredentialResponse,
Expand Down Expand Up @@ -901,6 +939,9 @@ func (f *Flow) handleIssuanceAck(

// getAuthorizationDetailsRequestBody returns authorization details request body
// either with credential_configuration_id or format params.
// If neither credential_configuration_id nor format supplied,
// Wallet CLI should use scope parameter to request credential type:
// https://openid.github.io/OpenID4VCI/openid-4-verifiable-credential-issuance-wg-draft.html#section-5.1.2
//
// Spec: https://openid.github.io/OpenID4VCI/openid-4-verifiable-credential-issuance-wg-draft.html#section-5.1.1
func (f *Flow) getAuthorizationDetailsRequestBody(
Expand Down Expand Up @@ -933,7 +974,8 @@ func (f *Flow) getAuthorizationDetailsRequestBody(
Type: "openid_credential",
}
default:
return nil, errors.New("neither credentialFormat nor credentialConfigurationID supplied")
// Valid case - neither credentialFormat nor credentialConfigurationID supplied.
return nil, nil
}

return json.Marshal(res)
Expand Down
2 changes: 1 addition & 1 deletion pkg/kms/aws/service_mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pkg/kms/mocks/kms_mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pkg/restapi/v1/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func ValidateVPFormat(format VPFormat) (vcsverifiable.Format, error) {
return vcsverifiable.Ldp, nil
}

return "", fmt.Errorf("unsupported vc format %s, use one of next [%s, %s]", format, JwtVcJsonLd, LdpVc)
return "", fmt.Errorf("unsupported vp format %s, use one of next [%s, %s]", format, JwtVcJsonLd, LdpVc)
}

func MapToVPFormat(format vcsverifiable.Format) (VPFormat, error) {
Expand Down
17 changes: 14 additions & 3 deletions pkg/restapi/v1/issuer/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,10 @@ func (c *Controller) PushAuthorizationDetails(ctx echo.Context) error {
return resterr.NewValidationError(resterr.InvalidValue, "authorization_details.format", err)
}

if errors.Is(err, resterr.ErrInvalidCredentialConfigurationID) {
return resterr.NewValidationError(resterr.InvalidValue, "authorization_details.credential_configuration_id", err)
}

return resterr.NewSystemError(resterr.IssuerOIDC4ciSvcComponent, "PushAuthorizationRequest", err)
}

Expand All @@ -572,9 +576,16 @@ func (c *Controller) prepareClaimDataAuthorizationRequest(
ctx context.Context,
body *PrepareClaimDataAuthorizationRequest,
) (*PrepareClaimDataAuthorizationResponse, error) {
ad, err := util.ValidateAuthorizationDetails(*body.AuthorizationDetails)
if err != nil {
return nil, err
var (
ad *oidc4ci.AuthorizationDetails
err error
)

if body.AuthorizationDetails != nil {
ad, err = util.ValidateAuthorizationDetails(*body.AuthorizationDetails)
if err != nil {
return nil, err
}
}

resp, err := c.oidc4ciService.PrepareClaimDataAuthorizationRequest(ctx,
Expand Down
52 changes: 50 additions & 2 deletions pkg/restapi/v1/issuer/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,18 @@ func TestController_PushAuthorizationDetails(t *testing.T) {
require.ErrorContains(t, err, "credential format not supported")
},
},
{
name: "CredentialConfigurationID not supported",
setup: func() {
mockOIDC4CISvc.EXPECT().PushAuthorizationDetails(gomock.Any(), "opState", gomock.Any()).Return(
resterr.ErrInvalidCredentialConfigurationID)

req = fmt.Sprintf(`{"op_state":"opState","authorization_details":%s}`, authorizationDetailsFormatBased) //nolint:lll
},
check: func(t *testing.T, err error) {
require.ErrorContains(t, err, "invalid credential configuration ID")
},
},
{
name: "Service error",
setup: func() {
Expand Down Expand Up @@ -1138,6 +1150,7 @@ func TestController_PrepareAuthorizationRequest(t *testing.T) {
assert.Nil(t, ad.CredentialDefinition.Context)
assert.NotNil(t, ad.CredentialDefinition.CredentialSubject)
assert.Equal(t, ad.CredentialDefinition.Type, []string{"VerifiableCredential", "UniversityDegreeCredential"})
assert.Equal(t, req.Scope, []string{"scope1", "scope2"})

return &oidc4ci.PrepareClaimDataAuthorizationResponse{
ProfileID: profileID,
Expand All @@ -1156,7 +1169,7 @@ func TestController_PrepareAuthorizationRequest(t *testing.T) {
profileSvc: mockProfileService,
}

req := fmt.Sprintf(`{"response_type":"code","op_state":"123","authorization_details":%s}`, authorizationDetailsFormatBased) //nolint:lll
req := fmt.Sprintf(`{"response_type":"code","op_state":"123","scope":["scope1", "scope2"],"authorization_details":%s}`, authorizationDetailsFormatBased) //nolint:lll
ctx := echoContext(withRequestBody([]byte(req)))
assert.NoError(t, c.PrepareAuthorizationRequest(ctx))
})
Expand All @@ -1177,6 +1190,41 @@ func TestController_PrepareAuthorizationRequest(t *testing.T) {
assert.Nil(t, ad.Locations)
assert.Equal(t, "UniversityDegreeCredential", ad.CredentialConfigurationID)
assert.Nil(t, ad.CredentialDefinition)
assert.Equal(t, req.Scope, []string{"scope1", "scope2"})

return &oidc4ci.PrepareClaimDataAuthorizationResponse{
ProfileID: profileID,
ProfileVersion: profileVersion,
}, nil
},
)

mockProfileService := NewMockProfileService(gomock.NewController(t))
mockProfileService.EXPECT().GetProfile(profileID, profileVersion).Return(&profileapi.Issuer{
OIDCConfig: &profileapi.OIDCConfig{},
}, nil)

c := &Controller{
oidc4ciService: mockOIDC4CIService,
profileSvc: mockProfileService,
}

req := fmt.Sprintf(`{"response_type":"code","op_state":"123","scope":["scope1", "scope2"],"authorization_details":%s}`, authorizationDetailsCredentialConfigurationIDBased) //nolint:lll
ctx := echoContext(withRequestBody([]byte(req)))
assert.NoError(t, c.PrepareAuthorizationRequest(ctx))
})

t.Run("Success scope based", func(t *testing.T) {
mockOIDC4CIService := NewMockOIDC4CIService(gomock.NewController(t))
mockOIDC4CIService.EXPECT().PrepareClaimDataAuthorizationRequest(gomock.Any(), gomock.Any()).DoAndReturn(
func(
ctx context.Context,
req *oidc4ci.PrepareClaimDataAuthorizationRequest,
) (*oidc4ci.PrepareClaimDataAuthorizationResponse, error) {
assert.Equal(t, "123", req.OpState)

assert.Nil(t, req.AuthorizationDetails)
assert.Equal(t, req.Scope, []string{"scope1", "scope2"})

return &oidc4ci.PrepareClaimDataAuthorizationResponse{
ProfileID: profileID,
Expand All @@ -1195,7 +1243,7 @@ func TestController_PrepareAuthorizationRequest(t *testing.T) {
profileSvc: mockProfileService,
}

req := fmt.Sprintf(`{"response_type":"code","op_state":"123","authorization_details":%s}`, authorizationDetailsCredentialConfigurationIDBased) //nolint:lll
req := `{"response_type":"code","op_state":"123","scope":["scope1", "scope2"]}`
ctx := echoContext(withRequestBody([]byte(req)))
assert.NoError(t, c.PrepareAuthorizationRequest(ctx))
})
Expand Down
24 changes: 3 additions & 21 deletions pkg/restapi/v1/oidc4ci/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ func (c *Controller) OidcAuthorize(e echo.Context, params OidcAuthorizeParams) e
},
}

var prepareAuthRequestAuthorizationDetails common.AuthorizationDetails
var prepareAuthRequestAuthorizationDetails *[]common.AuthorizationDetails

if params.AuthorizationDetails != nil {
var authorizationDetails []common.AuthorizationDetails
Expand All @@ -305,30 +305,12 @@ func (c *Controller) OidcAuthorize(e echo.Context, params OidcAuthorizeParams) e
}

// only single authorization_details supported for now.
prepareAuthRequestAuthorizationDetails = authorizationDetails[0]
} else {
// TODO: implement using scope parameter to request credential type
// https://openid.github.io/OpenID4VCI/openid-4-verifiable-credential-issuance-wg-draft.html#section-5.1.2

// prepareAuthRequestAuthorizationDetails = common.AuthorizationDetails{
// CredentialConfigurationId: nil,
// CredentialDefinition: &common.CredentialDefinition{
// Context: nil, // Not supported for now.
// CredentialSubject: nil, // Not supported for now.
// Type: scope,
// },
// Format: nil,
// Locations: nil, // Not supported for now.
// Type: "openid_credential",
// }

return resterr.NewValidationError(resterr.InvalidValue, "authorization_details",
errors.New("not supplied"))
prepareAuthRequestAuthorizationDetails = lo.ToPtr(authorizationDetails[:1])
}

r, err := c.issuerInteractionClient.PrepareAuthorizationRequest(ctx,
issuer.PrepareAuthorizationRequestJSONRequestBody{
AuthorizationDetails: lo.ToPtr([]common.AuthorizationDetails{prepareAuthRequestAuthorizationDetails}),
AuthorizationDetails: prepareAuthRequestAuthorizationDetails,
OpState: lo.FromPtr(params.IssuerState),
ResponseType: params.ResponseType,
Scope: lo.ToPtr([]string(ar.GetRequestedScopes())),
Expand Down
Loading

0 comments on commit 660207c

Please sign in to comment.