Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move OpenID4VCI code to own file #3144

Merged
merged 2 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 2 additions & 174 deletions auth/api/iam/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import (
"crypto"
"embed"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"html/template"
Expand All @@ -33,11 +32,9 @@ import (
"strings"
"time"

"github.com/google/uuid"
"github.com/labstack/echo/v4"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/go-did/vc"
"github.com/nuts-foundation/nuts-node/audit"
"github.com/nuts-foundation/nuts-node/auth"
"github.com/nuts-foundation/nuts-node/auth/api/iam/assets"
Expand Down Expand Up @@ -92,7 +89,7 @@ type Wrapper struct {
auth auth.AuthenticationServices
policyBackend policy.PDPBackend
storageEngine storage.Engine
JSONLDManager jsonld.JSONLD
jsonldManager jsonld.JSONLD
vcr vcr.VCR
vdr vdr.VDR
jwtSigner nutsCrypto.JWTSigner
Expand All @@ -114,7 +111,7 @@ func New(
storageEngine: storageEngine,
vcr: vcrInstance,
vdr: vdrInstance,
JSONLDManager: jsonldManager,
jsonldManager: jsonldManager,
jwtSigner: jwtSigner,
keyResolver: resolver.DIDKeyResolver{Resolver: vdrInstance.Resolver()},
jar: &jar{
Expand Down Expand Up @@ -707,148 +704,6 @@ func (r Wrapper) StatusList(ctx context.Context, request StatusListRequestObject
return StatusList200JSONResponse(*cred), nil
}

func (r Wrapper) RequestOid4vciCredentialIssuance(ctx context.Context, request RequestOid4vciCredentialIssuanceRequestObject) (RequestOid4vciCredentialIssuanceResponseObject, error) {
if request.Body == nil {
// why did oapi-codegen generate a pointer for the body??
return nil, core.InvalidInputError("missing request body")
}
// Parse and check the requester
requestHolder, err := r.toOwnedDID(ctx, request.Did)
if err != nil {
return nil, core.NotFoundError("requester DID: %w", err)
}

// Parse the issuer
issuerDid, err := did.ParseDID(request.Body.Issuer)
if err != nil {
return nil, core.InvalidInputError("could not parse Issuer DID: %s: %w", request.Body.Issuer, err)
}
// 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)
}
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("[]")
if len(request.Body.AuthorizationDetails) > 0 {
authorizationDetails, _ = json.Marshal(request.Body.AuthorizationDetails)
}
// Generate the state and PKCE
state := nutsCrypto.GenerateNonce()
pkceParams := generatePKCEParams()

// Figure out our own redirect URL by parsing the did:web and extracting the host.
requesterDidUrl, err := didweb.DIDToURL(*requestHolder)
if err != nil {
return nil, fmt.Errorf("failed convert did (%s) to url: %w", requestHolder.String(), err)
}
redirectUri, err := url.Parse(fmt.Sprintf("https://%s/iam/oid4vci/callback", requesterDidUrl.Host))
if err != nil {
return nil, fmt.Errorf("failed to create the url for host: %w", err)
}
// Store the session
err = r.openid4vciSessionStore().Put(state, &Oid4vciSession{
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.
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(),
oauth.AuthorizationDetailsParam: string(authorizationDetails),
oauth.RedirectURIParam: redirectUri.String(),
oauth.CodeChallengeParam: pkceParams.Challenge,
oauth.CodeChallengeMethodParam: pkceParams.ChallengeMethod,
})

return RequestOid4vciCredentialIssuance200JSONResponse{
RedirectURI: redirectUrl.String(),
}, nil
}

func (r Wrapper) CallbackOid4vciCredentialIssuance(ctx context.Context, request CallbackOid4vciCredentialIssuanceRequestObject) (CallbackOid4vciCredentialIssuanceResponseObject, error) {
state := request.Params.State
oid4vciSession := Oid4vciSession{}
err := r.openid4vciSessionStore().Get(state, &oid4vciSession)
if err != nil {
return nil, core.NotFoundError("Cannot locate active session for state: %s", state)
}
if request.Params.Error != nil {
errorCode := oauth.ErrorCode(*request.Params.Error)
errorDescription := ""
if request.Params.ErrorDescription != nil {
errorDescription = *request.Params.ErrorDescription
} else {
errorDescription = fmt.Sprintf("Issuer returned error code: %s", *request.Params.Error)
}
return nil, withCallbackURI(oauthError(errorCode, errorDescription), oid4vciSession.remoteRedirectUri())
}
code := request.Params.Code
pkceParams := oid4vciSession.PKCEParams
issuerDid := oid4vciSession.IssuerDid
holderDid := oid4vciSession.HolderDid
tokenEndpoint := oid4vciSession.IssuerTokenEndpoint
credentialEndpoint := oid4vciSession.IssuerCredentialEndpoint
if err != nil {
return nil, withCallbackURI(oauthError(oauth.ServerError, fmt.Sprintf("cannot fetch the right endpoints: %s", err.Error())), oid4vciSession.remoteRedirectUri())
}
response, err := r.auth.IAMClient().AccessToken(ctx, code, tokenEndpoint, oid4vciSession.RedirectUri, *holderDid, pkceParams.Verifier, false)
if err != nil {
return nil, withCallbackURI(oauthError(oauth.AccessDenied, fmt.Sprintf("error while fetching the access_token from endpoint: %s, error: %s", tokenEndpoint, err.Error())), oid4vciSession.remoteRedirectUri())
}
cNonce := response.Get(oauth.CNonceParam)
proofJWT, err := r.proofJwt(ctx, *holderDid, *issuerDid, &cNonce)
if err != nil {
return nil, withCallbackURI(oauthError(oauth.ServerError, fmt.Sprintf("error building proof to fetch the credential from endpoint %s, error: %s", credentialEndpoint, err.Error())), oid4vciSession.remoteRedirectUri())
}
credentials, err := r.auth.IAMClient().VerifiableCredentials(ctx, credentialEndpoint, response.AccessToken, proofJWT)
if err != nil {
return nil, withCallbackURI(oauthError(oauth.ServerError, fmt.Sprintf("error while fetching the credential from endpoint %s, error: %s", credentialEndpoint, err.Error())), oid4vciSession.remoteRedirectUri())
}
credential, err := vc.ParseVerifiableCredential(credentials.Credential)
if err != nil {
return nil, withCallbackURI(oauthError(oauth.ServerError, fmt.Sprintf("error while parsing the credential: %s, error: %s", credentials.Credential, err.Error())), oid4vciSession.remoteRedirectUri())
}
err = r.vcr.Verifier().Verify(*credential, true, true, nil)
if err != nil {
return nil, withCallbackURI(oauthError(oauth.ServerError, fmt.Sprintf("error while verifying the credential from issuer: %s, error: %s", credential.Issuer.String(), err.Error())), oid4vciSession.remoteRedirectUri())
}
err = r.vcr.Wallet().Put(ctx, *credential)
if err != nil {
return nil, withCallbackURI(oauthError(oauth.ServerError, fmt.Sprintf("error while storing credential with id: %s, error: %s", credential.ID, err.Error())), oid4vciSession.remoteRedirectUri())
}

log.Logger().Debugf("stored the credential with id: %s, now redirecting to %s", credential.ID, oid4vciSession.RemoteRedirectUri)

return CallbackOid4vciCredentialIssuance302Response{
Headers: CallbackOid4vciCredentialIssuance302ResponseHeaders{Location: oid4vciSession.RemoteRedirectUri},
}, nil
}

func (r Wrapper) openid4vciMetadata(ctx context.Context, issuerDid did.DID) (*oauth.OpenIDCredentialIssuerMetadata, *oauth.AuthorizationServerMetadata, error) {
oauthIssuer, err := didweb.DIDToURL(issuerDid)
if err != nil {
Expand Down Expand Up @@ -945,28 +800,6 @@ func (r Wrapper) createAuthorizationRequest(ctx context.Context, client did.DID,
return &redirectURL, nil
}

func (r *Wrapper) proofJwt(ctx context.Context, holderDid did.DID, audienceDid did.DID, nonce *string) (string, error) {
// TODO: is this the right key type?
kid, _, err := r.keyResolver.ResolveKey(holderDid, nil, resolver.NutsSigningKeyType)
if err != nil {
return "", fmt.Errorf("failed to resolve key for did (%s): %w", holderDid.String(), err)
}
jti, _ := uuid.NewUUID()
claims := map[string]interface{}{
"iss": holderDid.String(),
"aud": audienceDid.String(),
"jti": jti.String(),
}
if nonce != nil {
claims["nonce"] = nonce
}
proofJwt, err := r.jwtSigner.SignJWT(ctx, claims, nil, kid.String())
if err != nil {
return "", fmt.Errorf("failed to sign the JWT with kid (%s): %w", kid.String(), err)
}
return proofJwt, nil
}

// requestedDID constructs a did:web DID as it was requested by the API caller. It can be a DID with or without user path, e.g.:
// - did:web:example.com
// - did:web:example:iam:1234
Expand Down Expand Up @@ -1002,11 +835,6 @@ func (r Wrapper) authzRequestObjectStore() storage.SessionStore {
return r.storageEngine.GetSessionDatabase().GetStore(accessTokenValidity, oauthRequestObjectKey...)
}

// openid4vciSessionStore is used by the Client to keep track of OpenID4VCI requests
func (r Wrapper) openid4vciSessionStore() storage.SessionStore {
return r.storageEngine.GetSessionDatabase().GetStore(oid4vciSessionValidity, "openid4vci")
}

// createOAuth2BaseURL creates an OAuth2 base URL for an owned did:web DID
// It creates a URL in the following format: https://<did:web host>/oauth2/<did>
func createOAuth2BaseURL(webDID did.DID) (*url.URL, error) {
Expand Down
Loading
Loading