PASS3 OpenID4VP
OpenID4VP Diagram
OpenID4VP is an extension of OpenID Connect that allows the exchange of Verifiable Credentials (VC) between the Relying Party (RP) and the Identity Provider (IdP).
OpenID4VP in PASS3
Here is an example of using OpenID4VP in the Identity SDK with the authorization code
grant type
You can find the full example here
Package Install
Install Identity-SDK package
go get github.com/pass3id/identity-sdk/oi4vp
OAuth2 Client Configuration
The configuration of the OAuth2 client requires the client_id
and client_secret
, which can be obtained from PASS3.
var (
clientID = os.Getenv("PASS3_OAUTH2_CLIENT_ID")
clientSecret = os.Getenv("PASS3_OAUTH2_CLIENT_SECRET")
)
OpenID4VP Provider and Verifier
A provider is used to store settings from the authentication server, such as the auth URL
and token URL
.
A verifier is used to verify tokens obtained from the authentication server, such as access tokens
and VP tokens
.
There are two ways to initiate a provider.
1. Auto Lookup Config from OpenID Connect Discovery
ctx := context.Background()
provider, err := oi4vp.NewProvider(ctx, "https://pass3.id")
if err != nil {
// handle error
}
2. Manual Config
config := oi4vp.ProviderConfig{
AuthURL: "http://localhost:8080/authorize",
TokenURL: "http://localhost:8080/token",
}
provider := config.NewProvider(context.Background())
There are 2 verifier type:
TokenVerifier
for verify registered claims in token like expirations, issuer, audience, etc.VPTokenVerifier
for verify registerd claims and VCredential signature
Example:
verifier := oi4vp.NewVPVerifier(&oi4vp.Config{
SkipClientIDCheck: true,
SkipExpiryCheck: true,
})
_, err := verifier.Verify(context.Background(), vpToken)
if err != nil {
// handle error
}
// get claims from VP token
var claims struct {
VC map[string]interface{} `json:"vc"`
}
if err := verifier.Credentials[0].BuildClaims(&claims); err != nil {
t.Errorf("%v", err)
}
fmt.Println(claims.VC["id"])
fmt.Println(claims.VC["credentialSubject"])
Configure OAuth2 Client
Available credential type from PASS3 available in /oi4vp/credentials.go
.
oauth2Config := oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURL: "http://client.com/auth/pass3/callback",
// Discovery returns the OAuth2 endpoints.
Endpoint: provider.Endpoint(),
Scopes: []string{LinkedIdentifiers},
}
Create Endpoints Handler
We need two endpoints:
- The first endpoint is used for redirection when the user wants to initiate the "single sign-on" login process.
- The second endpoint is a callback endpoint that receives the authorization code from the provider.
login redirect endpoint
http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
state, err := randString(16)
if err != nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
nonce, err := randString(16)
if err != nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
setCallbackCookie(w, r, "state", state)
setCallbackCookie(w, r, "nonce", nonce)
http.Redirect(w, r, config.AuthCodeURL(state, oi4vp.Nonce(nonce)), http.StatusFound)
})
callback endpoint
In the callback endpoint handler, we can exchange the authorization code for an access token and VP token.
Then, we perform validation and verification on these tokens.
After that, we extract the claims
data from the verifiable presentation (VP) token. In this case, the credential is LinkedIdentifiers
.
http.HandleFunc("/auth/pass3/callback", func(w http.ResponseWriter, r *http.Request) {
state, err := r.Cookie("state")
if err != nil {
http.Error(w, "state not found", http.StatusBadRequest)
return
}
if r.URL.Query().Get("state") != state.Value {
http.Error(w, "state did not match", http.StatusBadRequest)
return
}
oauth2Token, err := config.Exchange(ctx, r.URL.Query().Get("code"))
if err != nil {
http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
return
}
vpToken, ok := oauth2Token.Extra("vp_token").(string)
if !ok {
http.Error(w, "No vp_token field in oauth2 token.", http.StatusInternalServerError)
return
}
token, err := verifier.Verify(ctx, accessToken)
if err != nil {
http.Error(w, "Failed to verify VP Token: "+err.Error(), http.StatusInternalServerError)
return
}
nonce, err := r.Cookie("nonce")
if err != nil {
http.Error(w, "nonce not found", http.StatusBadRequest)
return
}
if token.Nonce != nonce.Value {
http.Error(w, "nonce did not match", http.StatusBadRequest)
return
}
w.Write("jos")
})
Presentation Definition
The presentation definition
is used to define the credential that we want to request from the authentication server.
pd := &PresentationDefinition{
ID: "LinkedIdentifiers",
InputDescriptors: []InputDescriptor{
{
ID: "id card credential",
Format: map[string]any{
"ldp_vc": map[string]any{
"proof_type": []string{"Ed25519Signature2018"},
},
},
},
},
}
pdstr, _ := pd.String()
url := oauthConfig.AuthCodeURL(oauthState, oauth2.SetAuthURLParam("presentation_definition", pdstr))
Presentation Definition from URI
pd, _ := PresentationDefinitionFromURI("https://example.com/presentationdefs?ref=idcard_presentation_request")
pdstr, _ := pd.String()
url := oauthConfig.AuthCodeURL(oauthState, oauth2.SetAuthURLParam("presentation_definition", pdstr))
Presentation Submission
The presentation submission
contains mappings between the requested Verifiable Credentials and where to find them within the returned VP Token
// after get VP token from callback endpoint
token, err := oauthConfig.Exchange(context.Background(), code)
if err != nil {
// handle error
}
psstr := token.Extra("presentation_submission").(string)
ps, _ := PresentationSubmissionFromString(psstr)
fmt.Println(ps.ID)