This repository has been archived by the owner on Dec 12, 2024. It is now read-only.
generated from TBD54566975/tbd-project-template
-
Notifications
You must be signed in to change notification settings - Fork 52
Implement OIDC4VCI Credential Endpoint #369
Open
andresuribe87
wants to merge
36
commits into
TBD54566975:main
Choose a base branch
from
andresuribe87:OSE-435
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
36 commits
Select commit
Hold shift + click to select a range
6e64557
Add a simple server that hosts the credential issuer metadata.
andresuribe87 fcd2631
Move stuff. Make things secure. modules
andresuribe87 6bc45f9
Made the issuer metadata be user provided via a file that has a defau…
andresuribe87 c3bf59e
Simplified to only address the concern of the PR.
andresuribe87 aa2c208
Merge branch 'main' into OSE-438
andresuribe87 d52f4e2
Fixed module files
andresuribe87 4b5d2ff
Update go version for golangci-lint
andresuribe87 92793ad
Update golangci-lint version
andresuribe87 fd9c1b7
Trying with no cache
andresuribe87 7859382
Trying with no cache for golangci-lint, and cache for setting up.
andresuribe87 769a780
Skip all
andresuribe87 ba074b3
ssi-sdk is looking very suz
andresuribe87 d068b07
Revert "Simplified to only address the concern of the PR."
andresuribe87 8c0985f
Added the AuthEndpoint
andresuribe87 3017fae
Fix linter
andresuribe87 5c83bf4
Merge branch 'main' into OSE-433
andresuribe87 77be08e
Better structure.
andresuribe87 9f7ff48
Merge branch 'main' into OSE-433
decentralgabe 6d49df5
Add a simple server that hosts the credential issuer metadata.
andresuribe87 02a247f
Simplified to only address the concern of the PR.
andresuribe87 a771d0b
Trying with no cache
andresuribe87 b6dd7dc
Trying with no cache for golangci-lint, and cache for setting up.
andresuribe87 62f8f0b
Skip all
andresuribe87 0a2ef96
Revert "Simplified to only address the concern of the PR."
andresuribe87 a50d773
Added the AuthEndpoint
andresuribe87 af475fd
Fix linter
andresuribe87 e87bcfc
Better structure.
andresuribe87 c3d751f
Implement Credential Endpoint
andresuribe87 498ee8b
PR comments
andresuribe87 30229af
Issue created.
andresuribe87 bb9a671
Merge branch 'main' into OSE-433
andresuribe87 dc3b3ee
Mod fixing
andresuribe87 b8f3a4c
Merge branch 'OSE-433' into OSE-435
andresuribe87 e23773e
Merge branch 'main' into OSE-435
andresuribe87 8ef452c
Mod fixing
andresuribe87 2332326
package naming
andresuribe87 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,122 @@ | ||||||||||||||||
package middleware | ||||||||||||||||
|
||||||||||||||||
import ( | ||||||||||||||||
"context" | ||||||||||||||||
"fmt" | ||||||||||||||||
"io" | ||||||||||||||||
"net/http" | ||||||||||||||||
"net/url" | ||||||||||||||||
"strings" | ||||||||||||||||
|
||||||||||||||||
"github.com/goccy/go-json" | ||||||||||||||||
"github.com/pkg/errors" | ||||||||||||||||
"github.com/sirupsen/logrus" | ||||||||||||||||
"github.com/tbd54566975/ssi-service/pkg/server/framework" | ||||||||||||||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" | ||||||||||||||||
"golang.org/x/oauth2" | ||||||||||||||||
"golang.org/x/oauth2/clientcredentials" | ||||||||||||||||
) | ||||||||||||||||
|
||||||||||||||||
type introspecter struct { | ||||||||||||||||
// Introspection endpoint according to https://www.rfc-editor.org/rfc/rfc7662. | ||||||||||||||||
endpoint string | ||||||||||||||||
|
||||||||||||||||
// Config of the client credentials to use for authenticating with Endpoint. | ||||||||||||||||
conf clientcredentials.Config | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
func newIntrospect(endpoint string, config clientcredentials.Config) *introspecter { | ||||||||||||||||
return &introspecter{ | ||||||||||||||||
endpoint: endpoint, | ||||||||||||||||
conf: config, | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
// 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 commentThe reason will be displayed to describe this comment to others. Learn more. There is no plug in libraries for this? |
||||||||||||||||
// 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 commentThe reason will be displayed to describe this comment to others. Learn more. nit: |
||||||||||||||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}) | ||||||||||||||||
client := s.conf.Client(ctx) | ||||||||||||||||
// Send a request to the introspect endpoint to decide whether this is allowed. | ||||||||||||||||
authHeader := req.Header.Get("Authorization") | ||||||||||||||||
if !strings.HasPrefix(authHeader, "Bearer ") { | ||||||||||||||||
return errors.New("no bearer") | ||||||||||||||||
} | ||||||||||||||||
token := authHeader[len("Bearer "):] | ||||||||||||||||
|
||||||||||||||||
body := make(url.Values) | ||||||||||||||||
body.Set("token", token) | ||||||||||||||||
introspectionReq, err := http.NewRequestWithContext(ctx, http.MethodPost, s.endpoint, strings.NewReader(body.Encode())) | ||||||||||||||||
if err != nil { | ||||||||||||||||
return err | ||||||||||||||||
} | ||||||||||||||||
introspectionReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") | ||||||||||||||||
introspectionResp, err := client.Do(introspectionReq) | ||||||||||||||||
if err != nil { | ||||||||||||||||
return err | ||||||||||||||||
} | ||||||||||||||||
defer func(body io.ReadCloser) { | ||||||||||||||||
err := body.Close() | ||||||||||||||||
if err != nil { | ||||||||||||||||
logrus.WithError(err).Warn("closing body") | ||||||||||||||||
} | ||||||||||||||||
Comment on lines
+59
to
+62
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||
}(introspectionResp.Body) | ||||||||||||||||
|
||||||||||||||||
if introspectionResp.StatusCode != http.StatusOK { | ||||||||||||||||
return fmt.Errorf("status does not indicate success: code: %d, body: %v", introspectionResp.StatusCode, introspectionResp.Body) | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
result, err := extractIntrospectResult(introspectionResp.Body) | ||||||||||||||||
if err != nil { | ||||||||||||||||
return err | ||||||||||||||||
} | ||||||||||||||||
if !result.Active { | ||||||||||||||||
return errors.New("invalid token") | ||||||||||||||||
} | ||||||||||||||||
return nil | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
func extractIntrospectResult(r io.Reader) (*result, error) { | ||||||||||||||||
res := result{ | ||||||||||||||||
Optionals: make(map[string]json.RawMessage), | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
if err := json.NewDecoder(r).Decode(&res.Optionals); err != nil { | ||||||||||||||||
return nil, err | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
if val, ok := res.Optionals["active"]; ok { | ||||||||||||||||
if err := json.Unmarshal(val, &res.Active); err != nil { | ||||||||||||||||
return nil, err | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
delete(res.Optionals, "active") | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
return &res, nil | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
// result is the OAuth2 Introspection Result | ||||||||||||||||
type result struct { | ||||||||||||||||
Active bool | ||||||||||||||||
|
||||||||||||||||
Optionals map[string]json.RawMessage | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
// Introspect creates a middleware which can be used to gate access to protected resources. | ||||||||||||||||
// This middleware works by extracting the token from the `Authorization` header and then sending a request to the | ||||||||||||||||
// introspect endpoint (which should be compliant with https://www.rfc-editor.org/rfc/rfc7662) to obtain the | ||||||||||||||||
// whether the token is active. A `nil` error represents an active token. | ||||||||||||||||
// config represents the client credentials to use for authenticating with the introspect endpoint. | ||||||||||||||||
func Introspect(endpoint string, config clientcredentials.Config) framework.Middleware { | ||||||||||||||||
intro := newIntrospect(endpoint, config) | ||||||||||||||||
return func(handler framework.Handler) framework.Handler { | ||||||||||||||||
return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { | ||||||||||||||||
if err := intro.introspect(ctx, r); err != nil { | ||||||||||||||||
frameworkErr := framework.NewRequestErrorMsg("invalid_token", http.StatusUnauthorized) | ||||||||||||||||
return framework.RespondError(ctx, w, frameworkErr) | ||||||||||||||||
} | ||||||||||||||||
return handler(ctx, w, r) | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,93 @@ | ||||||
package middleware | ||||||
|
||||||
import ( | ||||||
"context" | ||||||
"io" | ||||||
"net/http" | ||||||
"net/http/httptest" | ||||||
"net/url" | ||||||
"strings" | ||||||
"testing" | ||||||
|
||||||
"github.com/stretchr/testify/require" | ||||||
"github.com/tbd54566975/ssi-service/pkg/testutil" | ||||||
"golang.org/x/oauth2" | ||||||
"golang.org/x/oauth2/clientcredentials" | ||||||
) | ||||||
|
||||||
func TestIntrospect(t *testing.T) { | ||||||
mockTokenServer := simpleOauthTokenServer() | ||||||
defer mockTokenServer.Close() | ||||||
conf := newConfig(mockTokenServer) | ||||||
|
||||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||||
w.WriteHeader(http.StatusOK) | ||||||
_, _ = w.Write([]byte(`{"active":true}`)) | ||||||
})) | ||||||
defer mockServer.Close() | ||||||
|
||||||
introspectMiddleware := Introspect(mockServer.URL, conf) | ||||||
|
||||||
handlerCalled := false | ||||||
handler := introspectMiddleware(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { | ||||||
handlerCalled = true | ||||||
return nil | ||||||
}) | ||||||
req := httptest.NewRequest(http.MethodPost, "/some_protected_url", strings.NewReader("")) | ||||||
req.Header.Set("Authorization", "Bearer my-awesome-token") | ||||||
require.NoError(t, handler(context.Background(), httptest.NewRecorder(), req)) | ||||||
require.True(t, handlerCalled) | ||||||
} | ||||||
|
||||||
func TestIntrospectReturnsError(t *testing.T) { | ||||||
mockTokenServer := simpleOauthTokenServer() | ||||||
defer mockTokenServer.Close() | ||||||
conf := newConfig(mockTokenServer) | ||||||
|
||||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||||
w.WriteHeader(http.StatusOK) | ||||||
_, _ = w.Write([]byte(`{"active":false}`)) | ||||||
})) | ||||||
defer mockServer.Close() | ||||||
|
||||||
introspectMiddleware := Introspect(mockServer.URL, conf) | ||||||
|
||||||
handler := introspectMiddleware(noOpHandler) | ||||||
req := httptest.NewRequest(http.MethodPost, "/some_protected_url", strings.NewReader("")) | ||||||
req.Header.Set("Authorization", "Bearer my-awesome-token") | ||||||
w := httptest.NewRecorder() | ||||||
err := handler(testutil.NewRequestContext(), w, req) | ||||||
require.NoError(t, err) | ||||||
assertCredentialErrorResponseEquals(t, w, `{"error":"invalid_token"}`) | ||||||
} | ||||||
|
||||||
func assertCredentialErrorResponseEquals(t *testing.T, w *httptest.ResponseRecorder, s string) { | ||||||
respBody, err := io.ReadAll(w.Body) | ||||||
require.NoError(t, err) | ||||||
require.JSONEq(t, s, string(respBody)) | ||||||
} | ||||||
|
||||||
func noOpHandler(context.Context, http.ResponseWriter, *http.Request) error { | ||||||
return nil | ||||||
} | ||||||
|
||||||
func simpleOauthTokenServer() *httptest.Server { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||||
w.Header().Set("Content-Type", "application/x-www-form-urlencoded") | ||||||
values := url.Values{} | ||||||
values.Set("access_token", "my-client-token") | ||||||
_, _ = w.Write([]byte(values.Encode())) | ||||||
})) | ||||||
} | ||||||
|
||||||
func newConfig(mockTokenServer *httptest.Server) clientcredentials.Config { | ||||||
conf := clientcredentials.Config{ | ||||||
ClientID: "my-test-client", | ||||||
ClientSecret: "", | ||||||
TokenURL: mockTokenServer.URL, | ||||||
Scopes: []string{"notsurewhatscope"}, | ||||||
EndpointParams: nil, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can remove this and client secret? |
||||||
AuthStyle: oauth2.AuthStyleInHeader, | ||||||
} | ||||||
return conf | ||||||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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?