-
Notifications
You must be signed in to change notification settings - Fork 53
Implement OIDC4VCI Credential Endpoint #369
base: main
Are you sure you want to change the base?
Conversation
Codecov Report
@@ Coverage Diff @@
## main #369 +/- ##
==========================================
- Coverage 20.23% 20.23% -0.01%
==========================================
Files 42 50 +8
Lines 4675 4928 +253
==========================================
+ Hits 946 997 +51
- Misses 3597 3790 +193
- Partials 132 141 +9
... and 6 files with indirect coverage changes Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. |
|
||
// Introspect extracts a token from the `Authorization` header, and determines whether it's active by using the | ||
// Endpoint configured. A `nil` error represents an active token. | ||
func (s introspecter) introspect(ctx context.Context, req *http.Request) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: i introspector
?
err := body.Close() | ||
if err != nil { | ||
logrus.WithError(err).Warn("closing body") | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
err := body.Close() | |
if err != nil { | |
logrus.WithError(err).Warn("closing body") | |
} | |
if err := body.Close(); err != nil { | |
logrus.WithError(err).Warn("closing body") | |
} |
@@ -0,0 +1,122 @@ | |||
package middleware |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am wondering if it may be confusing to co-locate server code for both binaries. what do you tink about having a duplicate file structure for oidc stuff? or, alliteratively a set of oidc
packages within existing packages?
return nil | ||
} | ||
|
||
func simpleOauthTokenServer() *httptest.Server { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
func simpleOauthTokenServer() *httptest.Server { | |
func simpleOAuthTokenServer() *httptest.Server { |
ClientSecret: "", | ||
TokenURL: mockTokenServer.URL, | ||
Scopes: []string{"notsurewhatscope"}, | ||
EndpointParams: nil, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can remove this and client secret?
} | ||
|
||
// CredentialError implements https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-response | ||
type CredentialError struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OIDCCredentialError
?
oidcRouter, err := router.NewOIDCCredentialRouter(oidc.NewOIDCService( | ||
didService.GetResolver(), | ||
credService, | ||
[32]byte{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
where's this data coming from?
err = oidcRouter.IssueCredential(newRequestContext(), w, req) | ||
require.NoError(t, err) | ||
|
||
var errResp2 map[string]any |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: favor make for maps
require.Equal(t, 120, credentialResponse.CNonceExpiresIn) | ||
|
||
// And do it again, but after the expiration time. We should now expect an error | ||
<-time.After(time.Duration(int64(errorResp["c_nonce_expires_in"].(float64)) * int64(time.Second))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
neat
var subject string | ||
var err error | ||
switch credRequest.Proof.ProofType { | ||
case "jwt": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we use a known type for this. think we have one in the sdk
Secret: serviceKey[:], | ||
}) | ||
if err != nil { | ||
panic(err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should return (*Service, error)
key, err := totp.Generate(totp.GenerateOpts{ | ||
Issuer: "ssi-service", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should these values come from config?
|
||
func (s Service) CredentialEndpoint(ctx context.Context, credRequest *model.CredentialRequest) (*model.CredentialResponse, error) { | ||
if credRequest.Format == "" { | ||
return nil, framework.NewRequestErrorMsg("invalid_request", http.StatusBadRequest) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would consider creating a const block of errors ...
const ( InvalidRequest OurErrorType = "invalid_request" ... )
return nil, errors.New("proof_type not recognized") | ||
} | ||
|
||
serviceResp, err := s.credService.GetCredentialsBySubject(ctx, credential.GetCredentialBySubjectRequest{Subject: subject}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this logic wasn't too straightforward for me to understand...we're getting all credentials for a subject, and then comparing the object model to see if it matches the passed in cred?
I think we can consider different paths for credential equality. if the entire object needs to match, can we simplify to the id
? What about a triple of id, schema, issuer
or issuanceDate
? id
is probably sufficient...
|
||
serviceResp, err := s.credService.GetCredentialsBySubject(ctx, credential.GetCredentialBySubjectRequest{Subject: subject}) | ||
if err != nil { | ||
return nil, err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wrap?
return nil, errors.New("no credential found") | ||
} | ||
|
||
func sameElements(arr1, arr2 []string) bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does reflect.DeepEquals
work as a replacement for this?
if len(message.Signatures()) != 1 { | ||
return "", errors.New("jwt expected to have exactly one signature") | ||
} | ||
headers := message.Signatures()[0].ProtectedHeaders() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
careful to avoid NPE if no signatures
headers := message.Signatures()[0].ProtectedHeaders() | ||
|
||
// - typ: REQUIRED. MUST be openid4vci-proof+jwt, which explicitly types the proof JWT as recommended in Section 3.11 of [RFC8725]. | ||
const openID4VCIType = "openid4vci-proof+jwt" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could this const be pulled out of the method defn?
func (s Service) isNonceValid(nonce string) bool { | ||
valid, err := totp.ValidateCustom(nonce, s.key.Secret(), time.Now(), s.validateOpts()) | ||
if err != nil { | ||
logrus.WithError(err).Error("Problem validating") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if there's an err should we return false?
what if valid == true and err == something?
} | ||
} | ||
|
||
// Introspect extracts a token from the `Authorization` header, and determines whether it's active by using the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no plug in libraries for this?
func (s *SSIServer) OIDCAPI(service svcframework.Service) error { | ||
r, err := router.NewOIDCCredentialRouter(service) | ||
if err != nil { | ||
return err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return err | |
return util.LoggingErrorMsg(err, "could not create OIDC router") | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There seems to be a lot low level code parsing url requests and stuff,
Are there any existing libraries we can use to do some of this heavy lifting?
My friend Chad gave me some examples:
Golang OAuth2 Library: https://pkg.go.dev/golang.org/x/oauth2
osin: https://github.com/RangelReale/osin
oauth2_proxy: https://github.com/oauth2-proxy/oauth2-proxy
Go-Guardian: https://github.com/shaj13/go-guardian
Overview
This change implements https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-endpoint
Description
In order to support this, some prerequisites were implemented:
introspect
that can be set on a per handler basis. It's responsible for gating access to the endpoint defined by the handler.The implementation itself includes:
How Has This Been Tested?
Multiple unit tests.
Checklist
Before submitting this PR, please make sure:
References
https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-endpoint