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
Open
Show file tree
Hide file tree
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 Mar 17, 2023
fcd2631
Move stuff. Make things secure. modules
andresuribe87 Mar 17, 2023
6bc45f9
Made the issuer metadata be user provided via a file that has a defau…
andresuribe87 Mar 17, 2023
c3bf59e
Simplified to only address the concern of the PR.
andresuribe87 Mar 23, 2023
aa2c208
Merge branch 'main' into OSE-438
andresuribe87 Mar 23, 2023
d52f4e2
Fixed module files
andresuribe87 Mar 23, 2023
4b5d2ff
Update go version for golangci-lint
andresuribe87 Mar 23, 2023
92793ad
Update golangci-lint version
andresuribe87 Mar 23, 2023
fd9c1b7
Trying with no cache
andresuribe87 Mar 23, 2023
7859382
Trying with no cache for golangci-lint, and cache for setting up.
andresuribe87 Mar 23, 2023
769a780
Skip all
andresuribe87 Mar 23, 2023
ba074b3
ssi-sdk is looking very suz
andresuribe87 Mar 23, 2023
d068b07
Revert "Simplified to only address the concern of the PR."
andresuribe87 Mar 23, 2023
8c0985f
Added the AuthEndpoint
andresuribe87 Mar 24, 2023
3017fae
Fix linter
andresuribe87 Mar 24, 2023
5c83bf4
Merge branch 'main' into OSE-433
andresuribe87 Mar 24, 2023
77be08e
Better structure.
andresuribe87 Mar 24, 2023
9f7ff48
Merge branch 'main' into OSE-433
decentralgabe Mar 24, 2023
6d49df5
Add a simple server that hosts the credential issuer metadata.
andresuribe87 Mar 17, 2023
02a247f
Simplified to only address the concern of the PR.
andresuribe87 Mar 23, 2023
a771d0b
Trying with no cache
andresuribe87 Mar 23, 2023
b6dd7dc
Trying with no cache for golangci-lint, and cache for setting up.
andresuribe87 Mar 23, 2023
62f8f0b
Skip all
andresuribe87 Mar 23, 2023
0a2ef96
Revert "Simplified to only address the concern of the PR."
andresuribe87 Mar 23, 2023
a50d773
Added the AuthEndpoint
andresuribe87 Mar 24, 2023
af475fd
Fix linter
andresuribe87 Mar 24, 2023
e87bcfc
Better structure.
andresuribe87 Mar 24, 2023
c3d751f
Implement Credential Endpoint
andresuribe87 Mar 30, 2023
498ee8b
PR comments
andresuribe87 Mar 30, 2023
30229af
Issue created.
andresuribe87 Mar 30, 2023
bb9a671
Merge branch 'main' into OSE-433
andresuribe87 Mar 30, 2023
dc3b3ee
Mod fixing
andresuribe87 Mar 30, 2023
b8f3a4c
Merge branch 'OSE-433' into OSE-435
andresuribe87 Mar 30, 2023
e23773e
Merge branch 'main' into OSE-435
andresuribe87 Mar 31, 2023
8ef452c
Mod fixing
andresuribe87 Mar 31, 2023
2332326
package naming
andresuribe87 Mar 31, 2023
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
29 changes: 21 additions & 8 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/joho/godotenv"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/oauth2/clientcredentials"
)

const (
Expand All @@ -35,6 +36,11 @@ type SSIServiceConfig struct {
Services ServicesConfig `toml:"services"`
}

type OIDCConfig struct {
IntrospectEndpoint string `toml:"introspect_endpoint" conf:"http://authserver/introspect"`
ClientCredentials clientcredentials.Config `toml:"client_credentials"`
}

// ServerConfig represents configurable properties for the HTTP server
type ServerConfig struct {
APIHost string `toml:"api_host" conf:"default:0.0.0.0:3000"`
Expand All @@ -48,6 +54,7 @@ type ServerConfig struct {
LogLevel string `toml:"log_level" conf:"default:debug"`
EnableSchemaCaching bool `toml:"enable_schema_caching" conf:"default:true"`
EnableAllowAllCORS bool `toml:"enable_allow_all_cors" conf:"default:false"`
OIDCConfig OIDCConfig `toml:"oidc_config"`
}

type IssuingServiceConfig struct {
Expand All @@ -61,6 +68,10 @@ func (s *IssuingServiceConfig) IsEmpty() bool {
return reflect.DeepEqual(s, &IssuingServiceConfig{})
}

type OIDCCredentialServiceConfig struct {
CNonceExpiresIn *time.Duration `toml:"c_nonce_expired_in,omitempty"`
}

// ServicesConfig represents configurable properties for the components of the SSI Service
type ServicesConfig struct {
// at present, it is assumed that a single storage provider works for all services
Expand All @@ -71,14 +82,15 @@ type ServicesConfig struct {
ServiceEndpoint string `toml:"service_endpoint"`

// Embed all service-specific configs here. The order matters: from which should be instantiated first, to last
KeyStoreConfig KeyStoreServiceConfig `toml:"keystore,omitempty"`
DIDConfig DIDServiceConfig `toml:"did,omitempty"`
IssuingServiceConfig IssuingServiceConfig `toml:"issuing,omitempty"`
SchemaConfig SchemaServiceConfig `toml:"schema,omitempty"`
CredentialConfig CredentialServiceConfig `toml:"credential,omitempty"`
ManifestConfig ManifestServiceConfig `toml:"manifest,omitempty"`
PresentationConfig PresentationServiceConfig `toml:"presentation,omitempty"`
WebhookConfig WebhookServiceConfig `toml:"webhook,omitempty"`
KeyStoreConfig KeyStoreServiceConfig `toml:"keystore,omitempty"`
DIDConfig DIDServiceConfig `toml:"did,omitempty"`
IssuingServiceConfig IssuingServiceConfig `toml:"issuing,omitempty"`
SchemaConfig SchemaServiceConfig `toml:"schema,omitempty"`
CredentialConfig CredentialServiceConfig `toml:"credential,omitempty"`
ManifestConfig ManifestServiceConfig `toml:"manifest,omitempty"`
PresentationConfig PresentationServiceConfig `toml:"presentation,omitempty"`
WebhookConfig WebhookServiceConfig `toml:"webhook,omitempty"`
OIDCCredentialConfig OIDCCredentialServiceConfig `toml:"oidc,omitempty"`
}

// BaseServiceConfig represents configurable properties for a specific component of the SSI Service
Expand Down Expand Up @@ -277,6 +289,7 @@ func loadDefaultServicesConfig(config *SSIServiceConfig) {
WebhookConfig: WebhookServiceConfig{
BaseServiceConfig: &BaseServiceConfig{Name: "webhook"},
},
OIDCCredentialConfig: OIDCCredentialServiceConfig{},
}

config.Services = servicesConfig
Expand Down
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ require (
github.com/google/uuid v1.3.0
github.com/joho/godotenv v1.5.1
github.com/lestrrat-go/jwx v1.2.25
github.com/lestrrat-go/jwx/v2 v2.0.9
github.com/magefile/mage v1.14.0
github.com/mr-tron/base58 v1.2.0
github.com/multiformats/go-multibase v0.2.0
github.com/multiformats/go-varint v0.0.7
github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852
github.com/ory/fosite v0.44.0
github.com/pkg/errors v0.9.1
github.com/pquerna/otp v1.4.0
github.com/redis/go-redis/extra/redisotel/v9 v9.0.2
github.com/redis/go-redis/v9 v9.0.2
github.com/rs/cors v1.8.3
Expand All @@ -38,6 +40,7 @@ require (
go.opentelemetry.io/otel/sdk v1.14.0
go.opentelemetry.io/otel/trace v1.14.0
golang.org/x/crypto v0.7.0
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
gopkg.in/go-playground/validator.v9 v9.31.0
)

Expand All @@ -48,6 +51,7 @@ require (
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
github.com/bits-and-blooms/bitset v1.5.0 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cristalhq/jwt/v4 v4.0.2 // indirect
github.com/dave/jennifer v1.4.0 // indirect
Expand Down Expand Up @@ -79,7 +83,6 @@ require (
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.4 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/jwx/v2 v2.0.9 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/magiconair/properties v1.8.1 // indirect
github.com/mattn/goveralls v0.0.6 // indirect
Expand Down Expand Up @@ -110,7 +113,6 @@ require (
go.opentelemetry.io/otel/metric v0.37.0 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ github.com/bits-and-blooms/bitset v1.5.0 h1:NpE8frKRLGHIcEzkR+gZhiioW1+WbYV6fKwD
github.com/bits-and-blooms/bitset v1.5.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/bmatcuk/doublestar/v2 v2.0.3/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQmTZRptLie8RgRw=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bsm/ginkgo/v2 v2.5.0 h1:aOAnND1T40wEdAtkGSkvSICWeQ8L3UASX7YVCqQx+eQ=
github.com/bsm/ginkgo/v2 v2.5.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
github.com/bsm/gomega v1.20.0 h1:JhAwLmtRzXFTx2AkALSLa8ijZafntmhSoU63Ok18Uq8=
Expand Down Expand Up @@ -871,6 +873,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc=
github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI=
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
Expand Down
122 changes: 122 additions & 0 deletions pkg/server/middleware/introspect.go
Original file line number Diff line number Diff line change
@@ -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?


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
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?

// 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 ?

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
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")
}

}(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)
}
}
}
93 changes: 93 additions & 0 deletions pkg/server/middleware/introspect_test.go
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 {
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 {

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,
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?

AuthStyle: oauth2.AuthStyleInHeader,
}
return conf
}
Loading