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

Implement OIDC4VCI Credential Endpoint #369

Open
wants to merge 36 commits into
base: main
Choose a base branch
from

Conversation

andresuribe87
Copy link
Contributor

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:

  • A middleware called introspect that can be set on a per handler basis. It's responsible for gating access to the endpoint defined by the handler.
  • Scaffolding for the endpoint.

The implementation itself includes:

  • Using a time based one time password for the nonce implementation. The seed key used is the serviceKey already stored in the keyService.
  • Models for the request and response.

How Has This Been Tested?

Multiple unit tests.

Checklist

Before submitting this PR, please make sure:

  • I have read the CONTRIBUTING document.
  • My code is consistent with the rest of the project
  • I have tagged the relevant reviewers and/or interested parties
  • I have updated the READMEs and other documentation of affected packages

References

https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-endpoint

@codecov-commenter
Copy link

codecov-commenter commented Mar 31, 2023

Codecov Report

Merging #369 (2332326) into main (362e4d2) will decrease coverage by 0.01%.
The diff coverage is 43.22%.

@@            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     
Impacted Files Coverage Δ
config/config.go 16.04% <0.00%> (-0.10%) ⬇️
pkg/server/router/oidc_credential.go 0.00% <0.00%> (ø)
pkg/service/keystore/service.go 39.35% <0.00%> (-0.52%) ⬇️
pkg/testutil/setup.go 0.00% <0.00%> (ø)
pkg/server/middleware/introspect.go 63.07% <63.07%> (ø)
pkg/server/server.go 80.82% <76.92%> (-0.25%) ⬇️

... 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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: i introspector ?

Comment on lines +59 to +62
err := body.Close()
if err != nil {
logrus.WithError(err).Warn("closing body")
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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
Copy link
Member

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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func simpleOauthTokenServer() *httptest.Server {
func simpleOAuthTokenServer() *httptest.Server {

ClientSecret: "",
TokenURL: mockTokenServer.URL,
Scopes: []string{"notsurewhatscope"},
EndpointParams: nil,
Copy link
Member

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 {
Copy link
Member

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{
Copy link
Member

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
Copy link
Member

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)))
Copy link
Member

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":
Copy link
Member

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)
Copy link
Member

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)

Comment on lines +74 to +75
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "ssi-service",
Copy link
Member

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)
Copy link
Member

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})
Copy link
Member

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
Copy link
Member

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 {
Copy link
Member

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()
Copy link
Member

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"
Copy link
Member

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")
Copy link
Member

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
Copy link
Contributor

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
Copy link
Contributor

@nitro-neal nitro-neal Apr 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return err
return util.LoggingErrorMsg(err, "could not create OIDC router")

Copy link
Contributor

@nitro-neal nitro-neal left a 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants