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

feat: support for wallet initiated flow #1370

Merged
merged 23 commits into from
Jul 18, 2023
Merged
Show file tree
Hide file tree
Changes from 19 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
268 changes: 135 additions & 133 deletions api/spec/openapi.gen.go

Large diffs are not rendered by default.

196 changes: 190 additions & 6 deletions component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"regexp"
"strings"
"time"

Expand All @@ -34,6 +36,7 @@ import (
"github.com/trustbloc/vcs/pkg/kms/signer"
"github.com/trustbloc/vcs/pkg/restapi/v1/common"
issuerv1 "github.com/trustbloc/vcs/pkg/restapi/v1/issuer"
"github.com/trustbloc/vcs/pkg/service/oidc4ci"
)

const (
Expand Down Expand Up @@ -66,7 +69,7 @@ func WithClientID(clientID string) OauthClientOpt {

func (s *Service) RunOIDC4CI(config *OIDC4CIConfig, hooks *Hooks) error {
log.Println("Starting OIDC4VCI authorized code flow")

ctx := context.Background()
log.Printf("Initiate issuance URL:\n\n\t%s\n\n", config.InitiateIssuanceURL)
offerResponse, err := credentialoffer.ParseInitiateIssuanceUrl(
config.InitiateIssuanceURL,
Expand All @@ -77,16 +80,16 @@ func (s *Service) RunOIDC4CI(config *OIDC4CIConfig, hooks *Hooks) error {
}

s.print("Getting issuer OIDC config")
oidcConfig, err := s.getIssuerOIDCConfig(offerResponse.CredentialIssuer)
oidcConfig, err := s.getIssuerOIDCConfig(ctx, offerResponse.CredentialIssuer)
if err != nil {
return fmt.Errorf("get issuer oidc config: %w", err)
return fmt.Errorf("get issuer OIDC config: %w", err)
}

oidcIssuerCredentialConfig, err := s.getIssuerCredentialsOIDCConfig(
offerResponse.CredentialIssuer,
)
if err != nil {
return fmt.Errorf("get issuer oidc issuer config: %w", err)
return fmt.Errorf("get issuer OIDC issuer config: %w", err)
}

redirectURL, err := url.Parse(config.RedirectURI)
Expand Down Expand Up @@ -160,7 +163,7 @@ func (s *Service) RunOIDC4CI(config *OIDC4CIConfig, hooks *Hooks) error {
return fmt.Errorf("auth code is empty")
}

ctx := context.WithValue(context.Background(), oauth2.HTTPClient, s.httpClient)
ctx = context.WithValue(ctx, oauth2.HTTPClient, s.httpClient)

var beforeTokenRequestHooks []OauthClientOpt

Expand Down Expand Up @@ -229,11 +232,192 @@ func (s *Service) RunOIDC4CI(config *OIDC4CIConfig, hooks *Hooks) error {
return nil
}

var matchRegex = regexp.MustCompile(oidc4ci.WalletInitFlowClaimRegex)

func extractIssuerURLFromScopes(scopes []string) (string, error) {
for _, scope := range scopes {
if matchRegex.MatchString(scope) {
DRK3 marked this conversation as resolved.
Show resolved Hide resolved
return scope, nil
}
}

return "", errors.New("issuer URL not found in scopes")
}

func (s *Service) RunOIDC4CIWalletInitiated(config *OIDC4CIConfig, hooks *Hooks) error {
log.Println("Starting OIDC4VCI authorized code flow Wallet initiated")
ctx := context.Background()

issuerUrl, err := extractIssuerURLFromScopes(config.Scope)
if err != nil {
return errors.New(
"undefined scopes supplied. " +
"Make sure one of the provided scope is in the VCS issuer URL format. ref " +
skynet2 marked this conversation as resolved.
Show resolved Hide resolved
oidc4ci.WalletInitFlowClaimRegex)
}

oidcIssuerCredentialConfig, err := s.getIssuerCredentialsOIDCConfig(
issuerUrl,
)
if err != nil {
return fmt.Errorf("get issuer OIDC issuer config: %w", err)
}

oidcConfig, err := s.getIssuerOIDCConfig(ctx, issuerUrl)
if err != nil {
return fmt.Errorf("get issuer OIDC config: %w", err)
}

redirectURL, err := url.Parse(config.RedirectURI)
if err != nil {
return fmt.Errorf("parse redirect url: %w", err)
}

var listener net.Listener

if config.Login == "" { // bind listener for callback server to support log in with a browser
listener, err = net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
return fmt.Errorf("listen: %w", err)
}

redirectURL.Host = fmt.Sprintf(
"%s:%d",
redirectURL.Hostname(),
listener.Addr().(*net.TCPAddr).Port,
)
}

s.oauthClient = &oauth2.Config{
ClientID: config.ClientID,
RedirectURL: redirectURL.String(),
Scopes: config.Scope,
Endpoint: oauth2.Endpoint{
AuthURL: oidcConfig.AuthorizationEndpoint,
TokenURL: oidcConfig.TokenEndpoint,
AuthStyle: oauth2.AuthStyleInHeader,
},
}

issuerState := uuid.New().String()
sudeshrshetty marked this conversation as resolved.
Show resolved Hide resolved
state := uuid.New().String()

b, err := json.Marshal(&common.AuthorizationDetails{
Type: "openid_credential",
Types: []string{
"VerifiableCredential",
config.CredentialType,
},
Format: lo.ToPtr(config.CredentialFormat),
})
if err != nil {
return fmt.Errorf("marshal authorization details: %w", err)
}

authCodeURL := s.oauthClient.AuthCodeURL(state,
oauth2.SetAuthURLParam("issuer_state", issuerState),
oauth2.SetAuthURLParam("code_challenge", "MLSjJIlPzeRQoN9YiIsSzziqEuBSmS4kDgI3NDjbfF8"),
oauth2.SetAuthURLParam("code_challenge_method", "S256"),
oauth2.SetAuthURLParam("authorization_details", string(b)),
)

var authCode string

if config.Login == "" { // interactive mode: login with a browser
authCode, err = s.getAuthCodeFromBrowser(listener, authCodeURL)
if err != nil {
return fmt.Errorf("get auth code from browser: %w", err)
}
} else {
authCode, err = s.getAuthCode(config, authCodeURL)
if err != nil {
return fmt.Errorf("get auth code: %w", err)
}
}

if authCode == "" {
return fmt.Errorf("auth code is empty")
}

ctx = context.WithValue(ctx, oauth2.HTTPClient, s.httpClient)

var beforeTokenRequestHooks []OauthClientOpt

if hooks != nil {
beforeTokenRequestHooks = hooks.BeforeTokenRequest
}

for _, f := range beforeTokenRequestHooks {
f(s.oauthClient)
}

s.print("Exchanging authorization code for access token")
token, err := s.oauthClient.Exchange(ctx, authCode,
oauth2.SetAuthURLParam("code_verifier", "xalsLDydJtHwIQZukUyj6boam5vMUaJRWv-BnGCAzcZi3ZTs"),
)
if err != nil {
return fmt.Errorf("exchange code for token: %w", err)
}

s.token = token

err = s.CreateWallet()
if err != nil {
return fmt.Errorf("create wallet: %w", err)
}

s.print("Getting credential")
vc, _, err := s.getCredential(
oidcIssuerCredentialConfig.CredentialEndpoint,
config.CredentialType,
config.CredentialFormat,
issuerUrl,
)
if err != nil {
return fmt.Errorf("get credential: %w", err)
}

b, err = json.Marshal(vc)
if err != nil {
return fmt.Errorf("marshal vc: %w", err)
}

s.print("Adding credential to wallet")
if err = s.wallet.Add(s.vcProviderConf.WalletParams.Token, wallet.Credential, b); err != nil {
return fmt.Errorf("add credential: %w", err)
}

vcParsed, err := verifiable.ParseCredential(b,
verifiable.WithDisabledProofCheck(),
verifiable.WithJSONLDDocumentLoader(
s.ariesServices.JSONLDDocumentLoader()))
if err != nil {
return fmt.Errorf("parse VC: %w", err)
}

log.Printf(
"Credential with ID [%s] and type [%v] added successfully",
vcParsed.ID,
config.CredentialType,
)

if !s.keepWalletOpen {
s.wallet.Close()
}

return nil
}

func (s *Service) getIssuerOIDCConfig(
ctx context.Context,
issuerURL string,
) (*issuerv1.WellKnownOpenIDConfiguration, error) {
// GET /issuer/{profileID}/{profileVersion}/.well-known/openid-configuration
resp, err := s.httpClient.Get(issuerURL + "/.well-known/openid-configuration")
req, err := http.NewRequestWithContext(ctx, "GET", issuerURL+"/.well-known/openid-configuration", nil)
if err != nil {
return nil, err
}
resp, err := s.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("get issuer well-known: %w", err)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package walletrunner

import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
Expand All @@ -29,6 +30,7 @@ import (
func (s *Service) RunOIDC4CIPreAuth(config *OIDC4CIConfig) (*verifiable.Credential, error) {
log.Println("Starting OIDC4VCI pre-authorized code flow")

ctx := context.Background()
log.Printf("Initiate issuance URL:\n\n\t%s\n\n", config.InitiateIssuanceURL)
offerResponse, err := credentialoffer.ParseInitiateIssuanceUrl(config.InitiateIssuanceURL, s.httpClient)
if err != nil {
Expand All @@ -37,7 +39,7 @@ func (s *Service) RunOIDC4CIPreAuth(config *OIDC4CIConfig) (*verifiable.Credenti

s.print("Getting issuer OIDC config")
startTime := time.Now()
oidcConfig, err := s.getIssuerOIDCConfig(offerResponse.CredentialIssuer)
oidcConfig, err := s.getIssuerOIDCConfig(ctx, offerResponse.CredentialIssuer)
s.perfInfo.VcsCIFlowDuration += time.Since(startTime) // oidc config
s.perfInfo.GetIssuerOIDCConfig = time.Since(startTime)

Expand All @@ -51,7 +53,7 @@ func (s *Service) RunOIDC4CIPreAuth(config *OIDC4CIConfig) (*verifiable.Credenti
s.perfInfo.GetIssuerCredentialsOIDCConfig = time.Since(startTime)

if err != nil {
return nil, fmt.Errorf("get issuer oidc issuer config: %w", err)
return nil, fmt.Errorf("get issuer OIDC issuer config: %w", err)
}

tokenEndpoint := oidcConfig.TokenEndpoint
Expand Down
27 changes: 27 additions & 0 deletions docs/v1/common.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,33 @@ components:
- orb
- web
- key
WalletInitiatedFlowData:
title: WalletInitiatedFlowData
type: object
nullable: true
properties:
profile_id:
type: string
profile_version:
type: string
op_state:
type: string
scopes:
type: array
nullable: true
items:
type: string
claim_endpoint:
type: string
credential_template_id:
type: string
required:
- profile_id
- profile_version
- scopes
- claim_endpoint
- credential_template_id
- op_state
AuthorizationDetails:
title: AuthorizationDetails
type: object
Expand Down
32 changes: 32 additions & 0 deletions docs/v1/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -800,12 +800,16 @@ components:
pre-authorized_grant_anonymous_access_supported:
type: boolean
description: JSON Boolean indicating whether the issuer accepts a Token Request with a Pre-Authorized Code but without a client id. The default is false.
wallet_initiated_auth_flow_supported:
type: boolean
description: JSON Boolean indicating whether the issuer profile supports wallet initiated flow in OIDC4CI. The default is false.
required:
- authorization_endpoint
- token_endpoint
- response_types_supported
- scopes_supported
- grant_types_supported
- wallet_initiated_auth_flow_supported
- pre-authorized_grant_anonymous_access_supported
WellKnownOpenIDIssuerConfiguration:
title: WellKnownOpenIDIssuerConfiguration response
Expand Down Expand Up @@ -1154,6 +1158,9 @@ components:
type: string
code:
type: string
wallet_initiated_flow:
$ref: ./common.yaml#/components/schemas/WalletInitiatedFlowData
nullable: true
required:
- op_state
- code
Expand Down Expand Up @@ -1224,6 +1231,8 @@ components:
tx_id:
type: string
description: Transaction ID to correlate upcoming authorization response.
wallet_initiated_flow:
$ref: ./common.yaml#/components/schemas/WalletInitiatedFlowData
required:
- authorization_request
- authorization_endpoint
Expand All @@ -1250,6 +1259,26 @@ components:
- client_id
- client_secret
- scope
WalletInitiatedFlowParameters:
title: WalletInitiatedFlowParameters
x-tags:
- issuer
type: object
description: If transaction was initiated by Wallet - object will contain initiate issuance profile-specific data.
properties:
profileID:
type: string
profileVersion:
type: string
claimEndpoint:
type: string
credentialTemplateID:
type: string
required:
- profileID
- profileVersion
- claimEndpoint
- credentialTemplateID
InitiateOIDC4CIRequest:
title: InitiateOIDC4CIRequest
type: object
Expand Down Expand Up @@ -1300,6 +1329,9 @@ components:
claim_data:
type: object
description: Required for Pre-Authorized Code Flow. VCS OIDC Service acts as OP for wallet applications
wallet_initiated_issuance:
type: boolean
description: Boolean flags indicates whether given transaction is initiated by Wallet.
x-tags:
- issuer
InitiateOIDC4CIResponse:
Expand Down
Loading
Loading