From 06dcac4c2f8bcb632acfab2bf9a967af60f090fd Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Mon, 19 Oct 2020 15:26:34 +0200 Subject: [PATCH 01/14] fix: remove signing key creation (when not found) --- example/internal/mock/storage.go | 5 +-- pkg/op/mock/storage.mock.go | 22 +++---------- pkg/op/op.go | 54 +------------------------------- pkg/op/signer.go | 3 ++ pkg/op/storage.go | 3 +- pkg/utils/sign.go | 4 +++ 6 files changed, 14 insertions(+), 77 deletions(-) diff --git a/example/internal/mock/storage.go b/example/internal/mock/storage.go index 9671ec7..ffddd28 100644 --- a/example/internal/mock/storage.go +++ b/example/internal/mock/storage.go @@ -157,15 +157,12 @@ func (s *AuthStorage) CreateToken(_ context.Context, authReq op.TokenRequest) (s func (s *AuthStorage) TerminateSession(_ context.Context, userID, clientID string) error { return nil } -func (s *AuthStorage) GetSigningKey(_ context.Context, keyCh chan<- jose.SigningKey, _ chan<- error, _ <-chan time.Time) { +func (s *AuthStorage) GetSigningKey(_ context.Context, keyCh chan<- jose.SigningKey) { keyCh <- jose.SigningKey{Algorithm: jose.RS256, Key: s.key} } func (s *AuthStorage) GetKey(_ context.Context) (*rsa.PrivateKey, error) { return s.key, nil } -func (s *AuthStorage) SaveNewKeyPair(ctx context.Context) error { - return nil -} func (s *AuthStorage) GetKeySet(_ context.Context) (*jose.JSONWebKeySet, error) { pubkey := s.key.Public() return &jose.JSONWebKeySet{ diff --git a/pkg/op/mock/storage.mock.go b/pkg/op/mock/storage.mock.go index 9e4963a..ea962d5 100644 --- a/pkg/op/mock/storage.mock.go +++ b/pkg/op/mock/storage.mock.go @@ -187,15 +187,15 @@ func (mr *MockStorageMockRecorder) GetPrivateClaimsFromScopes(arg0, arg1, arg2, } // GetSigningKey mocks base method -func (m *MockStorage) GetSigningKey(arg0 context.Context, arg1 chan<- jose.SigningKey, arg2 chan<- error, arg3 <-chan time.Time) { +func (m *MockStorage) GetSigningKey(arg0 context.Context, arg1 chan<- jose.SigningKey) { m.ctrl.T.Helper() - m.ctrl.Call(m, "GetSigningKey", arg0, arg1, arg2, arg3) + m.ctrl.Call(m, "GetSigningKey", arg0, arg1) } // GetSigningKey indicates an expected call of GetSigningKey -func (mr *MockStorageMockRecorder) GetSigningKey(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { +func (mr *MockStorageMockRecorder) GetSigningKey(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSigningKey", reflect.TypeOf((*MockStorage)(nil).GetSigningKey), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSigningKey", reflect.TypeOf((*MockStorage)(nil).GetSigningKey), arg0, arg1) } // GetUserinfoFromScopes mocks base method @@ -256,20 +256,6 @@ func (mr *MockStorageMockRecorder) SaveAuthCode(arg0, arg1, arg2 interface{}) *g return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveAuthCode", reflect.TypeOf((*MockStorage)(nil).SaveAuthCode), arg0, arg1, arg2) } -// SaveNewKeyPair mocks base method -func (m *MockStorage) SaveNewKeyPair(arg0 context.Context) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SaveNewKeyPair", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// SaveNewKeyPair indicates an expected call of SaveNewKeyPair -func (mr *MockStorageMockRecorder) SaveNewKeyPair(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveNewKeyPair", reflect.TypeOf((*MockStorage)(nil).SaveNewKeyPair), arg0) -} - // TerminateSession mocks base method func (m *MockStorage) TerminateSession(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() diff --git a/pkg/op/op.go b/pkg/op/op.go index bba7a14..1a93024 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -6,7 +6,6 @@ import ( "net/http" "time" - "github.com/caos/logging" "github.com/gorilla/handlers" "github.com/gorilla/mux" "github.com/gorilla/schema" @@ -132,7 +131,7 @@ func NewOpenIDProvider(ctx context.Context, config *Config, storage Storage, opO keyCh := make(chan jose.SigningKey) o.signer = NewSigner(ctx, storage, keyCh) - go EnsureKey(ctx, storage, keyCh, o.timer, o.retry) + go storage.GetSigningKey(ctx, keyCh) o.httpHandler = CreateRouter(o, o.interceptors...) @@ -282,36 +281,6 @@ func (o *openIDKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSig return payload, err } -func EnsureKey(ctx context.Context, storage Storage, keyCh chan<- jose.SigningKey, timer <-chan time.Time, retry func(int) (bool, int)) { - count := 0 - timer = time.After(0) - errCh := make(chan error) - go storage.GetSigningKey(ctx, keyCh, errCh, timer) - for { - select { - case <-ctx.Done(): - return - case err := <-errCh: - if err == nil { - continue - } - _, ok := err.(StorageNotFoundError) - if ok { - err := storage.SaveNewKeyPair(ctx) - if err == nil { - continue - } - } - ok, count = retry(count) - if ok { - timer = time.After(0) - continue - } - logging.Log("OP-n6ynVE").WithError(err).Panic("error in key signer") - } - } -} - type Option func(o *openidProvider) error func WithCustomAuthEndpoint(endpoint Endpoint) Option { @@ -382,27 +351,6 @@ func WithHttpInterceptors(interceptors ...HttpInterceptor) Option { } } -func WithRetry(max int, sleep time.Duration) Option { - return func(o *openidProvider) error { - o.retry = func(count int) (bool, int) { - count++ - if count == max { - return false, count - } - time.Sleep(sleep) - return true, count - } - return nil - } -} - -func WithTimer(timer <-chan time.Time) Option { - return func(o *openidProvider) error { - o.timer = timer - return nil - } -} - func buildInterceptor(interceptors ...HttpInterceptor) func(http.HandlerFunc) http.Handler { return func(handlerFunc http.HandlerFunc) http.Handler { handler := handlerFuncToHandler(handlerFunc) diff --git a/pkg/op/signer.go b/pkg/op/signer.go index 76bb9c7..939fe13 100644 --- a/pkg/op/signer.go +++ b/pkg/op/signer.go @@ -34,6 +34,9 @@ func (s *tokenSigner) Health(_ context.Context) error { if s.signer == nil { return errors.New("no signer") } + if string(s.alg) == "" { + return errors.New("no signing algorithm") + } return nil } diff --git a/pkg/op/storage.go b/pkg/op/storage.go index eba5003..92c88e6 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -20,9 +20,8 @@ type AuthStorage interface { TerminateSession(context.Context, string, string) error - GetSigningKey(context.Context, chan<- jose.SigningKey, chan<- error, <-chan time.Time) + GetSigningKey(context.Context, chan<- jose.SigningKey) GetKeySet(context.Context) (*jose.JSONWebKeySet, error) - SaveNewKeyPair(context.Context) error } type OPStorage interface { diff --git a/pkg/utils/sign.go b/pkg/utils/sign.go index e1efe61..5ebac43 100644 --- a/pkg/utils/sign.go +++ b/pkg/utils/sign.go @@ -2,6 +2,7 @@ package utils import ( "encoding/json" + "errors" "gopkg.in/square/go-jose.v2" ) @@ -15,6 +16,9 @@ func Sign(object interface{}, signer jose.Signer) (string, error) { } func SignPayload(payload []byte, signer jose.Signer) (string, error) { + if signer == nil { + return "", errors.New("missing signer") + } result, err := signer.Sign(payload) if err != nil { return "", err From 3acc62e79eaf24010ea5056c0d9d14ae3f4cb3ec Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Tue, 20 Oct 2020 07:39:36 +0200 Subject: [PATCH 02/14] cleanup --- pkg/op/mock/storage.mock.impl.go | 23 ++++------------------- pkg/op/storage.go | 4 ---- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/pkg/op/mock/storage.mock.impl.go b/pkg/op/mock/storage.mock.impl.go index de9dee9..441d4a0 100644 --- a/pkg/op/mock/storage.mock.impl.go +++ b/pkg/op/mock/storage.mock.impl.go @@ -38,12 +38,6 @@ func NewMockStorageAny(t *testing.T) op.Storage { return m } -func NewMockStorageSigningKeyError(t *testing.T) op.Storage { - m := NewStorage(t) - ExpectSigningKeyError(m) - return m -} - func NewMockStorageSigningKeyInvalid(t *testing.T) op.Storage { m := NewStorage(t) ExpectSigningKeyInvalid(m) @@ -89,19 +83,10 @@ func ExpectValidClientID(s op.Storage) { }) } -func ExpectSigningKeyError(s op.Storage) { - mockS := s.(*MockStorage) - mockS.EXPECT().GetSigningKey(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( - func(_ context.Context, keyCh chan<- jose.SigningKey, errCh chan<- error, _ <-chan bool) { - errCh <- errors.New("error") - }, - ) -} - func ExpectSigningKeyInvalid(s op.Storage) { mockS := s.(*MockStorage) - mockS.EXPECT().GetSigningKey(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( - func(_ context.Context, keyCh chan<- jose.SigningKey, errCh chan<- error, _ <-chan bool) { + mockS.EXPECT().GetSigningKey(gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, keyCh chan<- jose.SigningKey) { keyCh <- jose.SigningKey{} }, ) @@ -109,8 +94,8 @@ func ExpectSigningKeyInvalid(s op.Storage) { func ExpectSigningKey(s op.Storage) { mockS := s.(*MockStorage) - mockS.EXPECT().GetSigningKey(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( - func(_ context.Context, keyCh chan<- jose.SigningKey, errCh chan<- error, _ <-chan bool) { + mockS.EXPECT().GetSigningKey(gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, keyCh chan<- jose.SigningKey) { keyCh <- jose.SigningKey{Algorithm: jose.HS256, Key: []byte("key")} }, ) diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 92c88e6..ab2fda2 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -39,10 +39,6 @@ type Storage interface { Health(context.Context) error } -type StorageNotFoundError interface { - IsNotFound() -} - type AuthRequest interface { GetID() string GetACR() string From a1a21f0d596f105e67e6f4a212d07f2cdc144e00 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Fri, 8 Jan 2021 15:01:23 +0100 Subject: [PATCH 03/14] introspect --- pkg/oidc/introspection.go | 256 +++++++++++++++++++++++++++++++++++ pkg/op/config.go | 1 + pkg/op/discovery.go | 6 +- pkg/op/op.go | 17 ++- pkg/op/storage.go | 9 +- pkg/op/token_intospection.go | 58 ++++++++ 6 files changed, 341 insertions(+), 6 deletions(-) create mode 100644 pkg/oidc/introspection.go create mode 100644 pkg/op/token_intospection.go diff --git a/pkg/oidc/introspection.go b/pkg/oidc/introspection.go new file mode 100644 index 0000000..7fa13de --- /dev/null +++ b/pkg/oidc/introspection.go @@ -0,0 +1,256 @@ +package oidc + +import ( + "encoding/json" + "fmt" + "time" + + "golang.org/x/text/language" + + "github.com/caos/oidc/pkg/utils" +) + +type IntrospectionRequest struct { + Token string `schema:"token"` +} + +type IntrospectionResponse interface { + UserInfoSetter + SetActive(bool) + IsActive() bool +} + +func NewIntrospectionResponse() IntrospectionResponse { + return &introspectionResponse{} +} + +type introspectionResponse struct { + Active bool `json:"active"` + Subject string `json:"sub,omitempty"` + userInfoProfile + userInfoEmail + userInfoPhone + Address UserInfoAddress `json:"address,omitempty"` + + claims map[string]interface{} +} + +func (u *introspectionResponse) IsActive() bool { + return u.Active +} +func (u *introspectionResponse) GetSubject() string { + return u.Subject +} + +func (u *introspectionResponse) GetName() string { + return u.Name +} + +func (u *introspectionResponse) GetGivenName() string { + return u.GivenName +} + +func (u *introspectionResponse) GetFamilyName() string { + return u.FamilyName +} + +func (u *introspectionResponse) GetMiddleName() string { + return u.MiddleName +} + +func (u *introspectionResponse) GetNickname() string { + return u.Nickname +} + +func (u *introspectionResponse) GetProfile() string { + return u.Profile +} + +func (u *introspectionResponse) GetPicture() string { + return u.Picture +} + +func (u *introspectionResponse) GetWebsite() string { + return u.Website +} + +func (u *introspectionResponse) GetGender() Gender { + return u.Gender +} + +func (u *introspectionResponse) GetBirthdate() string { + return u.Birthdate +} + +func (u *introspectionResponse) GetZoneinfo() string { + return u.Zoneinfo +} + +func (u *introspectionResponse) GetLocale() language.Tag { + return u.Locale +} + +func (u *introspectionResponse) GetPreferredUsername() string { + return u.PreferredUsername +} + +func (u *introspectionResponse) GetEmail() string { + return u.Email +} + +func (u *introspectionResponse) IsEmailVerified() bool { + return u.EmailVerified +} + +func (u *introspectionResponse) GetPhoneNumber() string { + return u.PhoneNumber +} + +func (u *introspectionResponse) IsPhoneNumberVerified() bool { + return u.PhoneNumberVerified +} + +func (u *introspectionResponse) GetAddress() UserInfoAddress { + return u.Address +} + +func (u *introspectionResponse) GetClaim(key string) interface{} { + return u.claims[key] +} + +func (u *introspectionResponse) SetActive(active bool) { + u.Active = active +} + +func (u *introspectionResponse) SetSubject(sub string) { + u.Subject = sub +} + +func (u *introspectionResponse) SetName(name string) { + u.Name = name +} + +func (u *introspectionResponse) SetGivenName(name string) { + u.GivenName = name +} + +func (u *introspectionResponse) SetFamilyName(name string) { + u.FamilyName = name +} + +func (u *introspectionResponse) SetMiddleName(name string) { + u.MiddleName = name +} + +func (u *introspectionResponse) SetNickname(name string) { + u.Nickname = name +} + +func (u *introspectionResponse) SetUpdatedAt(date time.Time) { + u.UpdatedAt = Time(date) +} + +func (u *introspectionResponse) SetProfile(profile string) { + u.Profile = profile +} + +func (u *introspectionResponse) SetPicture(picture string) { + u.Picture = picture +} + +func (u *introspectionResponse) SetWebsite(website string) { + u.Website = website +} + +func (u *introspectionResponse) SetGender(gender Gender) { + u.Gender = gender +} + +func (u *introspectionResponse) SetBirthdate(birthdate string) { + u.Birthdate = birthdate +} + +func (u *introspectionResponse) SetZoneinfo(zoneInfo string) { + u.Zoneinfo = zoneInfo +} + +func (u *introspectionResponse) SetLocale(locale language.Tag) { + u.Locale = locale +} + +func (u *introspectionResponse) SetPreferredUsername(name string) { + u.PreferredUsername = name +} + +func (u *introspectionResponse) SetEmail(email string, verified bool) { + u.Email = email + u.EmailVerified = verified +} + +func (u *introspectionResponse) SetPhone(phone string, verified bool) { + u.PhoneNumber = phone + u.PhoneNumberVerified = verified +} + +func (u *introspectionResponse) SetAddress(address UserInfoAddress) { + u.Address = address +} + +func (u *introspectionResponse) AppendClaims(key string, value interface{}) { + if u.claims == nil { + u.claims = make(map[string]interface{}) + } + u.claims[key] = value +} + +func (i *introspectionResponse) MarshalJSON() ([]byte, error) { + type Alias introspectionResponse + a := &struct { + *Alias + Locale interface{} `json:"locale,omitempty"` + UpdatedAt int64 `json:"updated_at,omitempty"` + PreferredUsername string `json:"username,omitempty"` + }{ + Alias: (*Alias)(i), + } + if !i.Locale.IsRoot() { + a.Locale = i.Locale + } + if !time.Time(i.UpdatedAt).IsZero() { + a.UpdatedAt = time.Time(i.UpdatedAt).Unix() + } + a.PreferredUsername = i.PreferredUsername + i.PreferredUsername = "" + + b, err := json.Marshal(a) + if err != nil { + return nil, err + } + + if len(i.claims) == 0 { + return b, nil + } + + claims, err := json.Marshal(i.claims) + if err != nil { + return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims) + } + return utils.ConcatenateJSON(b, claims) +} + +func (i *introspectionResponse) UnmarshalJSON(data []byte) error { + type Alias introspectionResponse + a := &struct { + *Alias + UpdatedAt int64 `json:"update_at,omitempty"` + }{ + Alias: (*Alias)(i), + } + if err := json.Unmarshal(data, &a); err != nil { + return err + } + + i.UpdatedAt = Time(time.Unix(a.UpdatedAt, 0).UTC()) + + return nil +} diff --git a/pkg/op/config.go b/pkg/op/config.go index a2b831e..f4fbb97 100644 --- a/pkg/op/config.go +++ b/pkg/op/config.go @@ -13,6 +13,7 @@ type Configuration interface { Issuer() string AuthorizationEndpoint() Endpoint TokenEndpoint() Endpoint + IntrospectionEndpoint() Endpoint UserinfoEndpoint() Endpoint EndSessionEndpoint() Endpoint KeysEndpoint() Endpoint diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 4bc1272..3bec79b 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -22,9 +22,9 @@ func CreateDiscoveryConfig(c Configuration, s Signer) *oidc.DiscoveryConfigurati Issuer: c.Issuer(), AuthorizationEndpoint: c.AuthorizationEndpoint().Absolute(c.Issuer()), TokenEndpoint: c.TokenEndpoint().Absolute(c.Issuer()), - // IntrospectionEndpoint: c.Intro().Absolute(c.Issuer()), - UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()), - EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()), + IntrospectionEndpoint: c.IntrospectionEndpoint().Absolute(c.Issuer()), + UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()), + EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()), // CheckSessionIframe: c.TokenEndpoint().Absolute(c.Issuer())(c.CheckSessionIframe), JwksURI: c.KeysEndpoint().Absolute(c.Issuer()), ScopesSupported: Scopes(c), diff --git a/pkg/op/op.go b/pkg/op/op.go index d16848e..76d5fcc 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -21,7 +21,7 @@ const ( readinessEndpoint = "/ready" defaultAuthorizationEndpoint = "authorize" defaulTokenEndpoint = "oauth/token" - defaultIntrospectEndpoint = "introspect" + defaultIntrospectEndpoint = "oauth/introspect" defaultUserinfoEndpoint = "userinfo" defaultEndSessionEndpoint = "end_session" defaultKeysEndpoint = "keys" @@ -78,6 +78,7 @@ func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router router.Handle(o.AuthorizationEndpoint().Relative(), intercept(authorizeHandler(o))) router.NewRoute().Path(o.AuthorizationEndpoint().Relative()+"/callback").Queries("id", "{id}").Handler(intercept(authorizeCallbackHandler(o))) router.Handle(o.TokenEndpoint().Relative(), intercept(tokenHandler(o))) + router.HandleFunc(o.IntrospectionEndpoint().Relative(), introspectionHandler(o)) router.HandleFunc(o.UserinfoEndpoint().Relative(), userinfoHandler(o)) router.Handle(o.EndSessionEndpoint().Relative(), intercept(endSessionHandler(o))) router.HandleFunc(o.KeysEndpoint().Relative(), keysHandler(o)) @@ -166,6 +167,10 @@ func (o *openidProvider) TokenEndpoint() Endpoint { return o.endpoints.Token } +func (o *openidProvider) IntrospectionEndpoint() Endpoint { + return o.endpoints.Introspection +} + func (o *openidProvider) UserinfoEndpoint() Endpoint { return o.endpoints.Userinfo } @@ -332,6 +337,16 @@ func WithCustomTokenEndpoint(endpoint Endpoint) Option { } } +func WithCustomIntrospectionEndpoint(endpoint Endpoint) Option { + return func(o *openidProvider) error { + if err := endpoint.Validate(); err != nil { + return err + } + o.endpoints.Introspection = endpoint + return nil + } +} + func WithCustomUserinfoEndpoint(endpoint Endpoint) Option { return func(o *openidProvider) error { if err := endpoint.Validate(); err != nil { diff --git a/pkg/op/storage.go b/pkg/op/storage.go index eba5003..0a0cec2 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -28,10 +28,15 @@ type AuthStorage interface { type OPStorage interface { GetClientByClientID(ctx context.Context, clientID string) (Client, error) AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error - GetUserinfoFromScopes(ctx context.Context, userID, clientID string, scopes []string) (oidc.UserInfo, error) - GetUserinfoFromToken(ctx context.Context, tokenID, subject, origin string) (oidc.UserInfo, error) + SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error + SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]interface{}, error) GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) + + //deprecated: use GetUserinfoFromScopes instead + GetUserinfoFromScopes(ctx context.Context, userID, clientID string, scopes []string) (oidc.UserInfo, error) + //deprecated: use SetUserinfoFromToken instead + GetUserinfoFromToken(ctx context.Context, tokenID, subject, origin string) (oidc.UserInfo, error) } type Storage interface { diff --git a/pkg/op/token_intospection.go b/pkg/op/token_intospection.go new file mode 100644 index 0000000..e6a5d4a --- /dev/null +++ b/pkg/op/token_intospection.go @@ -0,0 +1,58 @@ +package op + +import ( + "errors" + "net/http" + + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/utils" +) + +type Introspector interface { + Decoder() utils.Decoder + Crypto() Crypto + Storage() Storage + AccessTokenVerifier() AccessTokenVerifier +} + +func introspectionHandler(introspector Introspector) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + Introspect(w, r, introspector) + } +} + +func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspector) { + //validate authorization + + response := oidc.NewIntrospectionResponse() + token, err := ParseTokenInrospectionRequest(r, introspector.Decoder()) + if err != nil { + utils.MarshalJSON(w, response) + return + } + tokenID, subject, ok := getTokenIDAndSubject(r.Context(), introspector, token) + if !ok { + utils.MarshalJSON(w, response) + return + } + err = introspector.Storage().SetUserinfoFromToken(r.Context(), response, tokenID, subject, r.Header.Get("origin")) + if err != nil { + utils.MarshalJSON(w, response) + return + } + response.SetActive(true) + utils.MarshalJSON(w, response) +} + +func ParseTokenInrospectionRequest(r *http.Request, decoder utils.Decoder) (string, error) { + err := r.ParseForm() + if err != nil { + return "", errors.New("unable to parse request") + } + req := new(oidc.IntrospectionRequest) + err = decoder.Decode(req, r.Form) + if err != nil { + return "", errors.New("unable to parse request") + } + return req.Token, nil +} From 50ab51bb4633fa1e2896f5376c4c3456213dd2b8 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Thu, 28 Jan 2021 08:41:36 +0100 Subject: [PATCH 04/14] introspect and client assertion --- example/internal/mock/storage.go | 12 ++++----- pkg/oidc/discovery.go | 43 +++++++++++++++++++------------ pkg/oidc/introspection.go | 9 +++---- pkg/oidc/token_request.go | 16 ++++++++---- pkg/op/client.go | 2 +- pkg/op/config.go | 1 + pkg/op/discovery.go | 12 ++++++--- pkg/op/mock/client.mock.go | 4 +-- pkg/op/mock/configuration.mock.go | 28 ++++++++++++++++++++ pkg/op/mock/storage.mock.go | 28 ++++++++++++++++++++ pkg/op/mock/storage.mock.impl.go | 12 ++++----- pkg/op/op.go | 12 ++++++--- pkg/op/storage.go | 1 + pkg/op/tokenrequest.go | 43 ++++++++++++++++++++++++++----- pkg/op/verifier_jwt_profile.go | 8 +++--- 15 files changed, 171 insertions(+), 60 deletions(-) diff --git a/example/internal/mock/storage.go b/example/internal/mock/storage.go index 8c1ab38..40a1f86 100644 --- a/example/internal/mock/storage.go +++ b/example/internal/mock/storage.go @@ -184,22 +184,22 @@ func (s *AuthStorage) GetClientByClientID(_ context.Context, id string) (op.Clie return nil, errors.New("not found") } var appType op.ApplicationType - var authMethod op.AuthMethod + var authMethod oidc.AuthMethod var accessTokenType op.AccessTokenType var responseTypes []oidc.ResponseType if id == "web" { appType = op.ApplicationTypeWeb - authMethod = op.AuthMethodBasic + authMethod = oidc.AuthMethodBasic accessTokenType = op.AccessTokenTypeBearer responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode} } else if id == "native" { appType = op.ApplicationTypeNative - authMethod = op.AuthMethodNone + authMethod = oidc.AuthMethodNone accessTokenType = op.AccessTokenTypeBearer responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode} } else { appType = op.ApplicationTypeUserAgent - authMethod = op.AuthMethodNone + authMethod = oidc.AuthMethodNone accessTokenType = op.AccessTokenTypeJWT responseTypes = []oidc.ResponseType{oidc.ResponseTypeIDToken, oidc.ResponseTypeIDTokenOnly} } @@ -229,7 +229,7 @@ func (s *AuthStorage) GetPrivateClaimsFromScopes(_ context.Context, _, _ string, type ConfClient struct { applicationType op.ApplicationType - authMethod op.AuthMethod + authMethod oidc.AuthMethod responseTypes []oidc.ResponseType ID string accessTokenType op.AccessTokenType @@ -262,7 +262,7 @@ func (c *ConfClient) ApplicationType() op.ApplicationType { return c.applicationType } -func (c *ConfClient) AuthMethod() op.AuthMethod { +func (c *ConfClient) AuthMethod() oidc.AuthMethod { return c.authMethod } diff --git a/pkg/oidc/discovery.go b/pkg/oidc/discovery.go index 9333ca9..4621a1f 100644 --- a/pkg/oidc/discovery.go +++ b/pkg/oidc/discovery.go @@ -5,21 +5,30 @@ const ( ) type DiscoveryConfiguration struct { - Issuer string `json:"issuer,omitempty"` - AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"` - TokenEndpoint string `json:"token_endpoint,omitempty"` - IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"` - UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"` - EndSessionEndpoint string `json:"end_session_endpoint,omitempty"` - CheckSessionIframe string `json:"check_session_iframe,omitempty"` - JwksURI string `json:"jwks_uri,omitempty"` - ScopesSupported []string `json:"scopes_supported,omitempty"` - ResponseTypesSupported []string `json:"response_types_supported,omitempty"` - ResponseModesSupported []string `json:"response_modes_supported,omitempty"` - GrantTypesSupported []string `json:"grant_types_supported,omitempty"` - SubjectTypesSupported []string `json:"subject_types_supported,omitempty"` - IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"` - TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported,omitempty"` - CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported,omitempty"` - ClaimsSupported []string `json:"claims_supported,omitempty"` + Issuer string `json:"issuer,omitempty"` + AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"` + TokenEndpoint string `json:"token_endpoint,omitempty"` + IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"` + UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"` + EndSessionEndpoint string `json:"end_session_endpoint,omitempty"` + CheckSessionIframe string `json:"check_session_iframe,omitempty"` + JwksURI string `json:"jwks_uri,omitempty"` + ScopesSupported []string `json:"scopes_supported,omitempty"` + ResponseTypesSupported []string `json:"response_types_supported,omitempty"` + ResponseModesSupported []string `json:"response_modes_supported,omitempty"` + GrantTypesSupported []string `json:"grant_types_supported,omitempty"` + SubjectTypesSupported []string `json:"subject_types_supported,omitempty"` + IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"` + TokenEndpointAuthMethodsSupported []AuthMethod `json:"token_endpoint_auth_methods_supported,omitempty"` + CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported,omitempty"` + ClaimsSupported []string `json:"claims_supported,omitempty"` } + +type AuthMethod string + +const ( + AuthMethodBasic AuthMethod = "client_secret_basic" + AuthMethodPost AuthMethod = "client_secret_post" + AuthMethodNone AuthMethod = "none" + AuthMethodPrivateKeyJWT AuthMethod = "private_key_jwt" +) diff --git a/pkg/oidc/introspection.go b/pkg/oidc/introspection.go index 7fa13de..6414bef 100644 --- a/pkg/oidc/introspection.go +++ b/pkg/oidc/introspection.go @@ -207,9 +207,9 @@ func (i *introspectionResponse) MarshalJSON() ([]byte, error) { type Alias introspectionResponse a := &struct { *Alias - Locale interface{} `json:"locale,omitempty"` - UpdatedAt int64 `json:"updated_at,omitempty"` - PreferredUsername string `json:"username,omitempty"` + Locale interface{} `json:"locale,omitempty"` + UpdatedAt int64 `json:"updated_at,omitempty"` + Username string `json:"username,omitempty"` }{ Alias: (*Alias)(i), } @@ -219,8 +219,7 @@ func (i *introspectionResponse) MarshalJSON() ([]byte, error) { if !time.Time(i.UpdatedAt).IsZero() { a.UpdatedAt = time.Time(i.UpdatedAt).Unix() } - a.PreferredUsername = i.PreferredUsername - i.PreferredUsername = "" + a.Username = i.PreferredUsername b, err := json.Marshal(a) if err != nil { diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go index 1312b18..0c5b70b 100644 --- a/pkg/oidc/token_request.go +++ b/pkg/oidc/token_request.go @@ -15,6 +15,10 @@ const ( //GrantTypeTokenExchange defines the grant_type `urn:ietf:params:oauth:grant-type:token-exchange` used for the OAuth Token Exchange Grant GrantTypeTokenExchange GrantType = "urn:ietf:params:oauth:grant-type:token-exchange" + + //ClientAssertionTypeJWTAssertion defines the client_assertion_type `urn:ietf:params:oauth:client-assertion-type:jwt-bearer` + //used for the OAuth JWT Profile Client Authentication + ClientAssertionTypeJWTAssertion = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" ) type GrantType string @@ -27,11 +31,13 @@ type TokenRequest interface { type TokenRequestType GrantType type AccessTokenRequest struct { - Code string `schema:"code"` - RedirectURI string `schema:"redirect_uri"` - ClientID string `schema:"client_id"` - ClientSecret string `schema:"client_secret"` - CodeVerifier string `schema:"code_verifier"` + Code string `schema:"code"` + RedirectURI string `schema:"redirect_uri"` + ClientID string `schema:"client_id"` + ClientSecret string `schema:"client_secret"` + CodeVerifier string `schema:"code_verifier"` + ClientAssertion string `schema:"client_assertion"` + ClientAssertionType string `schema:"client_assertion_type"` } func (a *AccessTokenRequest) GrantType() GrantType { diff --git a/pkg/op/client.go b/pkg/op/client.go index 6d0891c..79715b0 100644 --- a/pkg/op/client.go +++ b/pkg/op/client.go @@ -28,7 +28,7 @@ type Client interface { RedirectURIs() []string PostLogoutRedirectURIs() []string ApplicationType() ApplicationType - AuthMethod() AuthMethod + AuthMethod() oidc.AuthMethod ResponseTypes() []oidc.ResponseType LoginURL(string) string AccessTokenType() AccessTokenType diff --git a/pkg/op/config.go b/pkg/op/config.go index f4fbb97..7cb522a 100644 --- a/pkg/op/config.go +++ b/pkg/op/config.go @@ -20,6 +20,7 @@ type Configuration interface { AuthMethodPostSupported() bool CodeMethodS256Supported() bool + AuthMethodPrivateKeyJWTSupported() bool GrantTypeTokenExchangeSupported() bool GrantTypeJWTAuthorizationSupported() bool } diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 3bec79b..8708fbb 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -108,12 +108,16 @@ func SubjectTypes(c Configuration) []string { return []string{"public"} //TODO: config } -func AuthMethods(c Configuration) []string { - authMethods := []string{ - string(AuthMethodBasic), +func AuthMethods(c Configuration) []oidc.AuthMethod { + authMethods := []oidc.AuthMethod{ + oidc.AuthMethodNone, + oidc.AuthMethodBasic, } if c.AuthMethodPostSupported() { - authMethods = append(authMethods, string(AuthMethodPost)) + authMethods = append(authMethods, oidc.AuthMethodPost) + } + if c.AuthMethodPrivateKeyJWTSupported() { + authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT) } return authMethods } diff --git a/pkg/op/mock/client.mock.go b/pkg/op/mock/client.mock.go index 1a15624..9d5fe41 100644 --- a/pkg/op/mock/client.mock.go +++ b/pkg/op/mock/client.mock.go @@ -64,10 +64,10 @@ func (mr *MockClientMockRecorder) ApplicationType() *gomock.Call { } // AuthMethod mocks base method -func (m *MockClient) AuthMethod() op.AuthMethod { +func (m *MockClient) AuthMethod() oidc.AuthMethod { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AuthMethod") - ret0, _ := ret[0].(op.AuthMethod) + ret0, _ := ret[0].(oidc.AuthMethod) return ret0 } diff --git a/pkg/op/mock/configuration.mock.go b/pkg/op/mock/configuration.mock.go index ece747c..4f83f35 100644 --- a/pkg/op/mock/configuration.mock.go +++ b/pkg/op/mock/configuration.mock.go @@ -47,6 +47,20 @@ func (mr *MockConfigurationMockRecorder) AuthMethodPostSupported() *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthMethodPostSupported", reflect.TypeOf((*MockConfiguration)(nil).AuthMethodPostSupported)) } +// AuthMethodPrivateKeyJWTSupported mocks base method +func (m *MockConfiguration) AuthMethodPrivateKeyJWTSupported() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AuthMethodPrivateKeyJWTSupported") + ret0, _ := ret[0].(bool) + return ret0 +} + +// AuthMethodPrivateKeyJWTSupported indicates an expected call of AuthMethodPrivateKeyJWTSupported +func (mr *MockConfigurationMockRecorder) AuthMethodPrivateKeyJWTSupported() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthMethodPrivateKeyJWTSupported", reflect.TypeOf((*MockConfiguration)(nil).AuthMethodPrivateKeyJWTSupported)) +} + // AuthorizationEndpoint mocks base method func (m *MockConfiguration) AuthorizationEndpoint() op.Endpoint { m.ctrl.T.Helper() @@ -117,6 +131,20 @@ func (mr *MockConfigurationMockRecorder) GrantTypeTokenExchangeSupported() *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypeTokenExchangeSupported", reflect.TypeOf((*MockConfiguration)(nil).GrantTypeTokenExchangeSupported)) } +// IntrospectionEndpoint mocks base method +func (m *MockConfiguration) IntrospectionEndpoint() op.Endpoint { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IntrospectionEndpoint") + ret0, _ := ret[0].(op.Endpoint) + return ret0 +} + +// IntrospectionEndpoint indicates an expected call of IntrospectionEndpoint +func (mr *MockConfigurationMockRecorder) IntrospectionEndpoint() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IntrospectionEndpoint", reflect.TypeOf((*MockConfiguration)(nil).IntrospectionEndpoint)) +} + // Issuer mocks base method func (m *MockConfiguration) Issuer() string { m.ctrl.T.Helper() diff --git a/pkg/op/mock/storage.mock.go b/pkg/op/mock/storage.mock.go index 9e4963a..b9adcec 100644 --- a/pkg/op/mock/storage.mock.go +++ b/pkg/op/mock/storage.mock.go @@ -270,6 +270,34 @@ func (mr *MockStorageMockRecorder) SaveNewKeyPair(arg0 interface{}) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveNewKeyPair", reflect.TypeOf((*MockStorage)(nil).SaveNewKeyPair), arg0) } +// SetUserinfoFromScopes mocks base method +func (m *MockStorage) SetUserinfoFromScopes(arg0 context.Context, arg1 oidc.UserInfoSetter, arg2, arg3 string, arg4 []string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetUserinfoFromScopes", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetUserinfoFromScopes indicates an expected call of SetUserinfoFromScopes +func (mr *MockStorageMockRecorder) SetUserinfoFromScopes(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserinfoFromScopes", reflect.TypeOf((*MockStorage)(nil).SetUserinfoFromScopes), arg0, arg1, arg2, arg3, arg4) +} + +// SetUserinfoFromToken mocks base method +func (m *MockStorage) SetUserinfoFromToken(arg0 context.Context, arg1 oidc.UserInfoSetter, arg2, arg3, arg4 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetUserinfoFromToken", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetUserinfoFromToken indicates an expected call of SetUserinfoFromToken +func (mr *MockStorageMockRecorder) SetUserinfoFromToken(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserinfoFromToken", reflect.TypeOf((*MockStorage)(nil).SetUserinfoFromToken), arg0, arg1, arg2, arg3, arg4) +} + // TerminateSession mocks base method func (m *MockStorage) TerminateSession(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() diff --git a/pkg/op/mock/storage.mock.impl.go b/pkg/op/mock/storage.mock.impl.go index 29d0d15..2788c39 100644 --- a/pkg/op/mock/storage.mock.impl.go +++ b/pkg/op/mock/storage.mock.impl.go @@ -65,23 +65,23 @@ func ExpectValidClientID(s op.Storage) { mockS.EXPECT().GetClientByClientID(gomock.Any(), gomock.Any()).DoAndReturn( func(_ context.Context, id string) (op.Client, error) { var appType op.ApplicationType - var authMethod op.AuthMethod + var authMethod oidc.AuthMethod var accessTokenType op.AccessTokenType var responseTypes []oidc.ResponseType switch id { case "web_client": appType = op.ApplicationTypeWeb - authMethod = op.AuthMethodBasic + authMethod = oidc.AuthMethodBasic accessTokenType = op.AccessTokenTypeBearer responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode} case "native_client": appType = op.ApplicationTypeNative - authMethod = op.AuthMethodNone + authMethod = oidc.AuthMethodNone accessTokenType = op.AccessTokenTypeBearer responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode} case "useragent_client": appType = op.ApplicationTypeUserAgent - authMethod = op.AuthMethodBasic + authMethod = oidc.AuthMethodBasic accessTokenType = op.AccessTokenTypeJWT responseTypes = []oidc.ResponseType{oidc.ResponseTypeIDToken} } @@ -119,7 +119,7 @@ func ExpectSigningKey(s op.Storage) { type ConfClient struct { id string appType op.ApplicationType - authMethod op.AuthMethod + authMethod oidc.AuthMethod accessTokenType op.AccessTokenType responseTypes []oidc.ResponseType devMode bool @@ -145,7 +145,7 @@ func (c *ConfClient) ApplicationType() op.ApplicationType { return c.appType } -func (c *ConfClient) AuthMethod() op.AuthMethod { +func (c *ConfClient) AuthMethod() oidc.AuthMethod { return c.authMethod } diff --git a/pkg/op/op.go b/pkg/op/op.go index 76d5fcc..fa32a23 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -26,9 +26,10 @@ const ( defaultEndSessionEndpoint = "end_session" defaultKeysEndpoint = "keys" - AuthMethodBasic AuthMethod = "client_secret_basic" - AuthMethodPost AuthMethod = "client_secret_post" - AuthMethodNone AuthMethod = "none" + AuthMethodBasic AuthMethod = "client_secret_basic" + AuthMethodPost AuthMethod = "client_secret_post" + AuthMethodNone AuthMethod = "none" + AuthMethodPrivateKeyJWT AuthMethod = "private_key_jwt" CodeMethodS256 = "S256" ) @@ -90,6 +91,7 @@ type Config struct { CryptoKey [32]byte DefaultLogoutRedirectURI string CodeMethodS256 bool + AuthMethodPrivateKeyJWT bool } type endpoints struct { @@ -191,6 +193,10 @@ func (o *openidProvider) CodeMethodS256Supported() bool { return o.config.CodeMethodS256 } +func (o *openidProvider) AuthMethodPrivateKeyJWTSupported() bool { + return o.config.AuthMethodPrivateKeyJWT +} + func (o *openidProvider) GrantTypeTokenExchangeSupported() bool { return false } diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 0a0cec2..b5f1dfe 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -32,6 +32,7 @@ type OPStorage interface { SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]interface{}, error) GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) + //ValidateJWTProfileScopes(ctx context.Context, userID string, scope oidc.Scopes) (oidc.Scopes, error) //deprecated: use GetUserinfoFromScopes instead GetUserinfoFromScopes(ctx context.Context, userID, clientID string, scopes []string) (oidc.UserInfo, error) diff --git a/pkg/op/tokenrequest.go b/pkg/op/tokenrequest.go index c3860ff..0e295a3 100644 --- a/pkg/op/tokenrequest.go +++ b/pkg/op/tokenrequest.go @@ -18,6 +18,7 @@ type Exchanger interface { Signer() Signer Crypto() Crypto AuthMethodPostSupported() bool + AuthMethodPrivateKeyJWTSupported() bool GrantTypeTokenExchangeSupported() bool GrantTypeJWTAuthorizationSupported() bool } @@ -112,18 +113,30 @@ func ValidateAccessTokenRequest(ctx context.Context, tokenReq *oidc.AccessTokenR } func AuthorizeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger Exchanger) (AuthRequest, Client, error) { + if tokenReq.ClientAssertionType == oidc.ClientAssertionTypeJWTAssertion { + jwtExchanger, ok := exchanger.(JWTAuthorizationGrantExchanger) + if !ok || !exchanger.AuthMethodPrivateKeyJWTSupported() { + return nil, nil, errors.New("auth_method private_key_jwt not supported") + } + return AuthorizePrivateJWTKey(ctx, tokenReq, jwtExchanger) + } client, err := exchanger.Storage().GetClientByClientID(ctx, tokenReq.ClientID) if err != nil { return nil, nil, err } - if client.AuthMethod() == AuthMethodNone { + if client.AuthMethod() == oidc.AuthMethodNone { authReq, err := AuthorizeCodeChallenge(ctx, tokenReq, exchanger) return authReq, client, err } - if client.AuthMethod() == AuthMethodPost && !exchanger.AuthMethodPostSupported() { + if client.AuthMethod() == oidc.AuthMethodPost && !exchanger.AuthMethodPostSupported() { return nil, nil, errors.New("auth_method post not supported") } - err = AuthorizeClientIDSecret(ctx, tokenReq.ClientID, tokenReq.ClientSecret, exchanger.Storage()) + authReq, err := AuthorizeClientIDSecret(ctx, tokenReq.ClientID, tokenReq.ClientSecret, tokenReq.Code, exchanger.Storage()) + return authReq, client, err +} + +func AuthorizePrivateJWTKey(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger JWTAuthorizationGrantExchanger) (AuthRequest, Client, error) { + jwtReq, err := VerifyJWTAssertion(ctx, tokenReq.ClientAssertion, exchanger.JWTProfileVerifier()) if err != nil { return nil, nil, err } @@ -131,11 +144,26 @@ func AuthorizeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exc if err != nil { return nil, nil, ErrInvalidRequest("invalid code") } + client, err := exchanger.Storage().GetClientByClientID(ctx, jwtReq.Issuer) + if err != nil { + return nil, nil, err + } + if client.AuthMethod() != oidc.AuthMethodPrivateKeyJWT { + return nil, nil, ErrInvalidRequest("invalid_client") + } return authReq, client, nil } -func AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string, storage OPStorage) error { - return storage.AuthorizeClientIDSecret(ctx, clientID, clientSecret) +func AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret, code string, storage Storage) (AuthRequest, error) { + err := storage.AuthorizeClientIDSecret(ctx, clientID, clientSecret) + if err != nil { + return nil, err + } + authReq, err := storage.AuthRequestByCode(ctx, code) + if err != nil { + return nil, ErrInvalidRequest("invalid code") + } + return authReq, nil } func AuthorizeCodeChallenge(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exchanger Exchanger) (AuthRequest, error) { @@ -158,12 +186,15 @@ func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger JWTAuthorizati RequestError(w, r, err) } - tokenRequest, err := VerifyJWTAssertion(r.Context(), profileRequest, exchanger.JWTProfileVerifier()) + tokenRequest, err := VerifyJWTAssertion(r.Context(), profileRequest.Assertion, exchanger.JWTProfileVerifier()) if err != nil { RequestError(w, r, err) return } + //TODO: filter scopes + tokenRequest.Scopes = profileRequest.Scope + resp, err := CreateJWTTokenResponse(r.Context(), tokenRequest, exchanger) if err != nil { RequestError(w, r, err) diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go index 8a31253..b30bdc5 100644 --- a/pkg/op/verifier_jwt_profile.go +++ b/pkg/op/verifier_jwt_profile.go @@ -8,7 +8,6 @@ import ( "gopkg.in/square/go-jose.v2" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/oidc/grants/tokenexchange" ) type JWTProfileVerifier interface { @@ -48,9 +47,9 @@ func (v *jwtProfileVerifier) Offset() time.Duration { return v.offset } -func VerifyJWTAssertion(ctx context.Context, profileRequest *tokenexchange.JWTProfileRequest, v JWTProfileVerifier) (*oidc.JWTTokenRequest, error) { +func VerifyJWTAssertion(ctx context.Context, assertion string, v JWTProfileVerifier) (*oidc.JWTTokenRequest, error) { request := new(oidc.JWTTokenRequest) - payload, err := oidc.ParseToken(profileRequest.Assertion, request) + payload, err := oidc.ParseToken(assertion, request) if err != nil { return nil, err } @@ -73,10 +72,9 @@ func VerifyJWTAssertion(ctx context.Context, profileRequest *tokenexchange.JWTPr keySet := &jwtProfileKeySet{v.Storage(), request.Subject} - if err = oidc.CheckSignature(ctx, profileRequest.Assertion, payload, request, nil, keySet); err != nil { + if err = oidc.CheckSignature(ctx, assertion, payload, request, nil, keySet); err != nil { return nil, err } - request.Scopes = profileRequest.Scope return request, nil } From 960be5af1f99630e08201087852e6c603d7e4637 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Mon, 1 Feb 2021 17:17:40 +0100 Subject: [PATCH 05/14] introspect and client assertion --- example/client/api/api.go | 166 ++++++++++-------- pkg/oidc/code_challenge.go | 2 +- pkg/oidc/discovery.go | 34 ++-- .../grants/tokenexchange/tokenexchange.go | 20 --- pkg/oidc/introspection.go | 4 + pkg/oidc/jwt_profile.go | 18 ++ pkg/oidc/token.go | 63 ++++++- pkg/op/authrequest.go | 18 ++ pkg/op/discovery.go | 6 +- pkg/op/discovery_test.go | 4 +- pkg/op/op.go | 18 +- pkg/op/probes.go | 2 +- pkg/op/storage.go | 17 -- pkg/op/tokenrequest.go | 5 +- pkg/op/verifier_jwt_profile.go | 2 +- pkg/rp/key.go | 33 ++++ pkg/rp/relaying_party.go | 33 +++- pkg/rp/resource_server.go | 118 +++++++++++++ pkg/rp/tockenexchange.go | 6 +- 19 files changed, 413 insertions(+), 156 deletions(-) create mode 100644 pkg/oidc/jwt_profile.go create mode 100644 pkg/rp/key.go create mode 100644 pkg/rp/resource_server.go diff --git a/example/client/api/api.go b/example/client/api/api.go index 6e1b0bd..e5345a8 100644 --- a/example/client/api/api.go +++ b/example/client/api/api.go @@ -1,90 +1,102 @@ package main -// import ( -// "encoding/json" -// "fmt" -// "log" -// "net/http" -// "os" +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "strings" + "time" -// "github.com/caos/oidc/pkg/oidc" -// "github.com/caos/oidc/pkg/oidc/rp" -// "github.com/caos/utils/logging" -// ) + "github.com/gorilla/mux" + "github.com/sirupsen/logrus" -// const ( -// publicURL string = "/public" -// protectedURL string = "/protected" -// protectedExchangeURL string = "/protected/exchange" -// ) + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/rp" +) + +const ( + publicURL string = "/public" + protectedURL string = "/protected" + protectedClaimURL string = "/protected/{claim}/{value}" +) func main() { - // clientID := os.Getenv("CLIENT_ID") - // clientSecret := os.Getenv("CLIENT_SECRET") - // issuer := os.Getenv("ISSUER") - // port := os.Getenv("PORT") + keyPath := os.Getenv("KEY") + port := os.Getenv("PORT") - // // ctx := context.Background() + provider, err := rp.NewResourceServerFromKeyFile(keyPath) + if err != nil { + logrus.Fatalf("error creating provider %s", err.Error()) + } - // providerConfig := &oidc.ProviderConfig{ - // ClientID: clientID, - // ClientSecret: clientSecret, - // Issuer: issuer, - // } - // provider, err := rp.NewDefaultProvider(providerConfig) - // logging.Log("APP-nx6PeF").OnError(err).Panic("error creating provider") + router := mux.NewRouter() - // http.HandleFunc(publicURL, func(w http.ResponseWriter, r *http.Request) { - // w.Write([]byte("OK")) - // }) + //public url accessible without any authorization + //will print `OK` and current timestamp + router.HandleFunc(publicURL, func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("OK " + time.Now().String())) + }) - // http.HandleFunc(protectedURL, func(w http.ResponseWriter, r *http.Request) { - // ok, token := checkToken(w, r) - // if !ok { - // return - // } - // resp, err := provider.Introspect(r.Context(), token) - // if err != nil { - // http.Error(w, err.Error(), http.StatusForbidden) - // return - // } - // data, err := json.Marshal(resp) - // if err != nil { - // http.Error(w, err.Error(), http.StatusInternalServerError) - // return - // } - // w.Write(data) - // }) + //protected url which needs an active token + //will print the result of the introspection endpoint on success + router.HandleFunc(protectedURL, func(w http.ResponseWriter, r *http.Request) { + ok, token := checkToken(w, r) + if !ok { + return + } + resp, err := rp.Introspect(r.Context(), provider, token) + if err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + data, err := json.Marshal(resp) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Write(data) + }) - // http.HandleFunc(protectedExchangeURL, func(w http.ResponseWriter, r *http.Request) { - // ok, token := checkToken(w, r) - // if !ok { - // return - // } - // tokens, err := provider.DelegationTokenExchange(r.Context(), token, oidc.WithResource([]string{"Test"})) - // if err != nil { - // http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized) - // return - // } + //protected url which needs an active token and checks if the response of the introspect endpoint + //contains a requested claim with the required (string) value + //e.g. /protected/username/livio@caos.ch + router.HandleFunc(protectedClaimURL, func(w http.ResponseWriter, r *http.Request) { + ok, token := checkToken(w, r) + if !ok { + return + } + resp, err := rp.Introspect(r.Context(), provider, token) + if err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + params := mux.Vars(r) + requestedClaim := params["claim"] + requestedValue := params["value"] + value, ok := resp.GetClaim(requestedClaim).(string) + if !ok || value == "" || value != requestedValue { + http.Error(w, "claim does not match", http.StatusForbidden) + return + } + w.Write([]byte("authorized with value " + value)) + }) - // data, err := json.Marshal(tokens) - // if err != nil { - // http.Error(w, err.Error(), http.StatusInternalServerError) - // return - // } - // w.Write(data) - // }) - - // lis := fmt.Sprintf("127.0.0.1:%s", port) - // log.Printf("listening on http://%s/", lis) - // log.Fatal(http.ListenAndServe(lis, nil)) - // } - - // func checkToken(w http.ResponseWriter, r *http.Request) (bool, string) { - // token := r.Header.Get("authorization") - // if token == "" { - // http.Error(w, "Auth header missing", http.StatusUnauthorized) - // return false, "" - // } - // return true, token + lis := fmt.Sprintf("127.0.0.1:%s", port) + log.Printf("listening on http://%s/", lis) + log.Fatal(http.ListenAndServe(lis, router)) +} + +func checkToken(w http.ResponseWriter, r *http.Request) (bool, string) { + auth := r.Header.Get("authorization") + if auth == "" { + http.Error(w, "auth header missing", http.StatusUnauthorized) + return false, "" + } + if !strings.HasPrefix(auth, oidc.PrefixBearer) { + http.Error(w, "invalid header", http.StatusUnauthorized) + return false, "" + } + return true, strings.TrimPrefix(auth, oidc.PrefixBearer) } diff --git a/pkg/oidc/code_challenge.go b/pkg/oidc/code_challenge.go index 44a0499..9c4c8a3 100644 --- a/pkg/oidc/code_challenge.go +++ b/pkg/oidc/code_challenge.go @@ -24,7 +24,7 @@ func NewSHACodeChallenge(code string) string { func VerifyCodeChallenge(c *CodeChallenge, codeVerifier string) bool { if c == nil { - return false //TODO: ? + return false } if c.Method == CodeChallengeMethodS256 { codeVerifier = NewSHACodeChallenge(codeVerifier) diff --git a/pkg/oidc/discovery.go b/pkg/oidc/discovery.go index 4621a1f..970369c 100644 --- a/pkg/oidc/discovery.go +++ b/pkg/oidc/discovery.go @@ -5,23 +5,23 @@ const ( ) type DiscoveryConfiguration struct { - Issuer string `json:"issuer,omitempty"` - AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"` - TokenEndpoint string `json:"token_endpoint,omitempty"` - IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"` - UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"` - EndSessionEndpoint string `json:"end_session_endpoint,omitempty"` - CheckSessionIframe string `json:"check_session_iframe,omitempty"` - JwksURI string `json:"jwks_uri,omitempty"` - ScopesSupported []string `json:"scopes_supported,omitempty"` - ResponseTypesSupported []string `json:"response_types_supported,omitempty"` - ResponseModesSupported []string `json:"response_modes_supported,omitempty"` - GrantTypesSupported []string `json:"grant_types_supported,omitempty"` - SubjectTypesSupported []string `json:"subject_types_supported,omitempty"` - IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"` - TokenEndpointAuthMethodsSupported []AuthMethod `json:"token_endpoint_auth_methods_supported,omitempty"` - CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported,omitempty"` - ClaimsSupported []string `json:"claims_supported,omitempty"` + Issuer string `json:"issuer,omitempty"` + AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"` + TokenEndpoint string `json:"token_endpoint,omitempty"` + IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"` + UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"` + EndSessionEndpoint string `json:"end_session_endpoint,omitempty"` + CheckSessionIframe string `json:"check_session_iframe,omitempty"` + JwksURI string `json:"jwks_uri,omitempty"` + ScopesSupported []string `json:"scopes_supported,omitempty"` + ResponseTypesSupported []string `json:"response_types_supported,omitempty"` + ResponseModesSupported []string `json:"response_modes_supported,omitempty"` + GrantTypesSupported []string `json:"grant_types_supported,omitempty"` + SubjectTypesSupported []string `json:"subject_types_supported,omitempty"` + IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"` + TokenEndpointAuthMethodsSupported []AuthMethod `json:"token_endpoint_auth_methods_supported,omitempty"` + CodeChallengeMethodsSupported []CodeChallengeMethod `json:"code_challenge_methods_supported,omitempty"` + ClaimsSupported []string `json:"claims_supported,omitempty"` } type AuthMethod string diff --git a/pkg/oidc/grants/tokenexchange/tokenexchange.go b/pkg/oidc/grants/tokenexchange/tokenexchange.go index 5cb6e79..02a9808 100644 --- a/pkg/oidc/grants/tokenexchange/tokenexchange.go +++ b/pkg/oidc/grants/tokenexchange/tokenexchange.go @@ -1,9 +1,5 @@ package tokenexchange -import ( - "github.com/caos/oidc/pkg/oidc" -) - const ( AccessTokenType = "urn:ietf:params:oauth:token-type:access_token" RefreshTokenType = "urn:ietf:params:oauth:token-type:refresh_token" @@ -26,22 +22,6 @@ type TokenExchangeRequest struct { requestedTokenType string `schema:"requested_token_type"` } -type JWTProfileRequest struct { - Assertion string `schema:"assertion"` - Scope oidc.Scopes `schema:"scope"` - GrantType oidc.GrantType `schema:"grant_type"` -} - -//ClientCredentialsGrantBasic creates an oauth2 `Client Credentials` Grant -//sneding client_id and client_secret as basic auth header -func NewJWTProfileRequest(assertion string, scopes ...string) *JWTProfileRequest { - return &JWTProfileRequest{ - GrantType: oidc.GrantTypeBearer, - Assertion: assertion, - Scope: scopes, - } -} - func NewTokenExchangeRequest(subjectToken, subjectTokenType string, opts ...TokenExchangeOption) *TokenExchangeRequest { t := &TokenExchangeRequest{ grantType: TokenExchangeGrantType, diff --git a/pkg/oidc/introspection.go b/pkg/oidc/introspection.go index 6414bef..98f3969 100644 --- a/pkg/oidc/introspection.go +++ b/pkg/oidc/introspection.go @@ -251,5 +251,9 @@ func (i *introspectionResponse) UnmarshalJSON(data []byte) error { i.UpdatedAt = Time(time.Unix(a.UpdatedAt, 0).UTC()) + if err := json.Unmarshal(data, &i.claims); err != nil { + return err + } + return nil } diff --git a/pkg/oidc/jwt_profile.go b/pkg/oidc/jwt_profile.go new file mode 100644 index 0000000..6969783 --- /dev/null +++ b/pkg/oidc/jwt_profile.go @@ -0,0 +1,18 @@ +package oidc + +type JWTProfileGrantRequest struct { + Assertion string `schema:"assertion"` + Scope Scopes `schema:"scope"` + GrantType GrantType `schema:"grant_type"` +} + +//NewJWTProfileGrantRequest creates an oauth2 `JSON Web Token (JWT) Profile` Grant +//`urn:ietf:params:oauth:grant-type:jwt-bearer` +//sending a self-signed jwt as assertion +func NewJWTProfileGrantRequest(assertion string, scopes ...string) *JWTProfileGrantRequest { + return &JWTProfileGrantRequest{ + GrantType: GrantTypeBearer, + Assertion: assertion, + Scope: scopes, + } +} diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index ff8f33e..1fc40b3 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -1,7 +1,10 @@ package oidc import ( + "crypto/rsa" + "crypto/x509" "encoding/json" + "encoding/pem" "io/ioutil" "time" @@ -14,6 +17,8 @@ import ( const ( //BearerToken defines the token_type `Bearer`, which is returned in a successful token response BearerToken = "Bearer" + + PrefixBearer = BearerToken + " " ) type Tokens struct { @@ -397,7 +402,7 @@ type AccessTokenResponse struct { type JWTProfileAssertion struct { PrivateKeyID string `json:"-"` PrivateKey []byte `json:"-"` - Issuer string `json:"issuer"` + Issuer string `json:"iss"` Subject string `json:"sub"` Audience Audience `json:"aud"` Expiration Time `json:"exp"` @@ -412,6 +417,19 @@ func NewJWTProfileAssertionFromKeyJSON(filename string, audience []string) (*JWT return NewJWTProfileAssertionFromFileData(data, audience) } +func NewJWTProfileAssertionStringFromFileData(data []byte, audience []string) (string, error) { + keyData := new(struct { + KeyID string `json:"keyId"` + Key string `json:"key"` + UserID string `json:"userId"` + }) + err := json.Unmarshal(data, keyData) + if err != nil { + return "", err + } + return generateJWTProfileToken(NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key))) +} + func NewJWTProfileAssertionFromFileData(data []byte, audience []string) (*JWTProfileAssertion, error) { keyData := new(struct { KeyID string `json:"keyId"` @@ -454,3 +472,46 @@ func AppendClientIDToAudience(clientID string, audience []string) []string { } return append(audience, clientID) } + +func generateJWTProfileToken(assertion *JWTProfileAssertion) (string, error) { + privateKey, err := bytesToPrivateKey(assertion.PrivateKey) + if err != nil { + return "", err + } + key := jose.SigningKey{ + Algorithm: jose.RS256, + Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.PrivateKeyID}, + } + signer, err := jose.NewSigner(key, &jose.SignerOptions{}) + if err != nil { + return "", err + } + + marshalledAssertion, err := json.Marshal(assertion) + if err != nil { + return "", err + } + signedAssertion, err := signer.Sign(marshalledAssertion) + if err != nil { + return "", err + } + return signedAssertion.CompactSerialize() +} + +func bytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) { + block, _ := pem.Decode(priv) + enc := x509.IsEncryptedPEMBlock(block) + b := block.Bytes + var err error + if enc { + b, err = x509.DecryptPEMBlock(block, nil) + if err != nil { + return nil, err + } + } + key, err := x509.ParsePKCS1PrivateKey(b) + if err != nil { + return nil, err + } + return key, nil +} diff --git a/pkg/op/authrequest.go b/pkg/op/authrequest.go index 9e320f8..3a79b9b 100644 --- a/pkg/op/authrequest.go +++ b/pkg/op/authrequest.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "strings" + "time" "github.com/gorilla/mux" @@ -12,6 +13,23 @@ import ( "github.com/caos/oidc/pkg/utils" ) +type AuthRequest interface { + GetID() string + GetACR() string + GetAMR() []string + GetAudience() []string + GetAuthTime() time.Time + GetClientID() string + GetCodeChallenge() *oidc.CodeChallenge + GetNonce() string + GetRedirectURI() string + GetResponseType() oidc.ResponseType + GetScopes() []string + GetState() string + GetSubject() string + Done() bool +} + type Authorizer interface { Storage() Storage Decoder() utils.Decoder diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 8708fbb..2ac4833 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -122,10 +122,10 @@ func AuthMethods(c Configuration) []oidc.AuthMethod { return authMethods } -func CodeChallengeMethods(c Configuration) []string { - codeMethods := make([]string, 0, 1) +func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod { + codeMethods := make([]oidc.CodeChallengeMethod, 0, 1) if c.CodeMethodS256Supported() { - codeMethods = append(codeMethods, CodeMethodS256) + codeMethods = append(codeMethods, oidc.CodeChallengeMethodS256) } return codeMethods } diff --git a/pkg/op/discovery_test.go b/pkg/op/discovery_test.go index c14fac4..e479faa 100644 --- a/pkg/op/discovery_test.go +++ b/pkg/op/discovery_test.go @@ -215,7 +215,7 @@ func Test_AuthMethods(t *testing.T) { m.EXPECT().AuthMethodPostSupported().Return(false) return m }()}, - []string{string(op.AuthMethodBasic)}, + []string{string(oidc.AuthMethodBasic)}, }, { "basic and post", @@ -223,7 +223,7 @@ func Test_AuthMethods(t *testing.T) { m.EXPECT().AuthMethodPostSupported().Return(true) return m }()}, - []string{string(op.AuthMethodBasic), string(op.AuthMethodPost)}, + []string{string(oidc.AuthMethodBasic), string(oidc.AuthMethodPost)}, }, } for _, tt := range tests { diff --git a/pkg/op/op.go b/pkg/op/op.go index fa32a23..ee41630 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -17,27 +17,27 @@ import ( ) const ( - healthzEndpoint = "/healthz" + healthEndpoint = "/healthz" readinessEndpoint = "/ready" defaultAuthorizationEndpoint = "authorize" - defaulTokenEndpoint = "oauth/token" + defaultTokenEndpoint = "oauth/token" defaultIntrospectEndpoint = "oauth/introspect" defaultUserinfoEndpoint = "userinfo" defaultEndSessionEndpoint = "end_session" defaultKeysEndpoint = "keys" - AuthMethodBasic AuthMethod = "client_secret_basic" - AuthMethodPost AuthMethod = "client_secret_post" - AuthMethodNone AuthMethod = "none" - AuthMethodPrivateKeyJWT AuthMethod = "private_key_jwt" + //AuthMethodBasic AuthMethod = "client_secret_basic" + //AuthMethodPost AuthMethod = "client_secret_post" + //AuthMethodNone AuthMethod = "none" + //AuthMethodPrivateKeyJWT AuthMethod = "private_key_jwt" - CodeMethodS256 = "S256" + //CodeMethodS256 = "S256" ) var ( DefaultEndpoints = &endpoints{ Authorization: NewEndpoint(defaultAuthorizationEndpoint), - Token: NewEndpoint(defaulTokenEndpoint), + Token: NewEndpoint(defaultTokenEndpoint), Introspection: NewEndpoint(defaultIntrospectEndpoint), Userinfo: NewEndpoint(defaultUserinfoEndpoint), EndSession: NewEndpoint(defaultEndSessionEndpoint), @@ -73,7 +73,7 @@ func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router handlers.AllowedHeaders([]string{"authorization", "content-type"}), handlers.AllowedOriginValidator(allowAllOrigins), )) - router.HandleFunc(healthzEndpoint, healthzHandler) + router.HandleFunc(healthEndpoint, healthHandler) router.HandleFunc(readinessEndpoint, readyHandler(o.Probes())) router.HandleFunc(oidc.DiscoveryEndpoint, discoveryHandler(o, o.Signer())) router.Handle(o.AuthorizationEndpoint().Relative(), intercept(authorizeHandler(o))) diff --git a/pkg/op/probes.go b/pkg/op/probes.go index 7dc00a9..c6bb748 100644 --- a/pkg/op/probes.go +++ b/pkg/op/probes.go @@ -10,7 +10,7 @@ import ( type ProbesFn func(context.Context) error -func healthzHandler(w http.ResponseWriter, r *http.Request) { +func healthHandler(w http.ResponseWriter, r *http.Request) { ok(w) } diff --git a/pkg/op/storage.go b/pkg/op/storage.go index b5f1dfe..c4da464 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -50,23 +50,6 @@ type StorageNotFoundError interface { IsNotFound() } -type AuthRequest interface { - GetID() string - GetACR() string - GetAMR() []string - GetAudience() []string - GetAuthTime() time.Time - GetClientID() string - GetCodeChallenge() *oidc.CodeChallenge - GetNonce() string - GetRedirectURI() string - GetResponseType() oidc.ResponseType - GetScopes() []string - GetState() string - GetSubject() string - Done() bool -} - type EndSessionRequest struct { UserID string Client Client diff --git a/pkg/op/tokenrequest.go b/pkg/op/tokenrequest.go index 0e295a3..1345005 100644 --- a/pkg/op/tokenrequest.go +++ b/pkg/op/tokenrequest.go @@ -7,7 +7,6 @@ import ( "net/url" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/oidc/grants/tokenexchange" "github.com/caos/oidc/pkg/utils" ) @@ -203,12 +202,12 @@ func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger JWTAuthorizati utils.MarshalJSON(w, resp) } -func ParseJWTProfileRequest(r *http.Request, decoder utils.Decoder) (*tokenexchange.JWTProfileRequest, error) { +func ParseJWTProfileRequest(r *http.Request, decoder utils.Decoder) (*oidc.JWTProfileGrantRequest, error) { err := r.ParseForm() if err != nil { return nil, ErrInvalidRequest("error parsing form") } - tokenReq := new(tokenexchange.JWTProfileRequest) + tokenReq := new(oidc.JWTProfileGrantRequest) err = decoder.Decode(tokenReq, r.Form) if err != nil { return nil, ErrInvalidRequest("error decoding form") diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go index b30bdc5..03d8264 100644 --- a/pkg/op/verifier_jwt_profile.go +++ b/pkg/op/verifier_jwt_profile.go @@ -70,7 +70,7 @@ func VerifyJWTAssertion(ctx context.Context, assertion string, v JWTProfileVerif //TODO: implement delegation (openid core / oauth rfc) } - keySet := &jwtProfileKeySet{v.Storage(), request.Subject} + keySet := &jwtProfileKeySet{v.Storage(), request.Issuer} if err = oidc.CheckSignature(ctx, assertion, payload, request, nil, keySet); err != nil { return nil, err diff --git a/pkg/rp/key.go b/pkg/rp/key.go new file mode 100644 index 0000000..58a92d1 --- /dev/null +++ b/pkg/rp/key.go @@ -0,0 +1,33 @@ +package rp + +import ( + "encoding/json" + "io/ioutil" +) + +const ( + serviceAccountKey = "serviceaccount" + applicationKey = "application" +) + +type keyFile struct { + Type string `json:"type"` // serviceaccount or application + KeyID string `json:"keyId"` + Key string `json:"key"` + Issuer string `json:"issuer"` + ClientID string `json:"clientId"` + //TokenURL string `json:"token_uri"` + //ProjectID string `json:"project_id"` +} + +func ConfigFromKeyFile(path string) (*keyFile, error) { + data, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + var f keyFile + if err := json.Unmarshal(data, &f); err != nil { + return nil, err + } + return &f, nil +} diff --git a/pkg/rp/relaying_party.go b/pkg/rp/relaying_party.go index 6807221..be62762 100644 --- a/pkg/rp/relaying_party.go +++ b/pkg/rp/relaying_party.go @@ -53,6 +53,9 @@ type RelayingParty interface { //IsOAuth2Only specifies whether relaying party handles only oauth2 or oidc calls IsOAuth2Only() bool + ClientKey() []byte + ClientKeyID() string + //IDTokenVerifier returns the verifier interface used for oidc id_token verification IDTokenVerifier() IDTokenVerifier @@ -74,11 +77,13 @@ type relayingParty struct { oauthConfig *oauth2.Config oauth2Only bool pkce bool + clientKey []byte + clientKeyID string httpClient *http.Client cookieHandler *utils.CookieHandler - errorHandler func(http.ResponseWriter, *http.Request, string, string, string) + errorHandler func(http.ResponseWriter, *http.Request, string, string, string) idTokenVerifier IDTokenVerifier verifierOpts []VerifierOption } @@ -103,6 +108,14 @@ func (rp *relayingParty) IsOAuth2Only() bool { return rp.oauth2Only } +func (rp *relayingParty) ClientKey() []byte { + return rp.clientKey +} + +func (rp *relayingParty) ClientKeyID() string { + return rp.clientKeyID +} + func (rp *relayingParty) IDTokenVerifier() IDTokenVerifier { if rp.idTokenVerifier == nil { rp.idTokenVerifier = NewIDTokenVerifier(rp.issuer, rp.oauthConfig.ClientID, NewRemoteKeySet(rp.httpClient, rp.endpoints.JKWsURL), rp.verifierOpts...) @@ -314,6 +327,14 @@ func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc } codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier)) } + //if len(rp.ClientKey()) > 0 { + // assertion, err := oidc.NewJWTProfileAssertionStringFromFileData(rp.ClientKey(), []string{rp.OAuthConfig().Endpoint.TokenURL}) + // if err != nil { + // http.Error(w, "failed to build assertion: "+err.Error(), http.StatusUnauthorized) + // return + // } + // codeOpts = append(codeOpts, WithClientAssertionJWT(assertion)) + //} tokens, err := CodeExchange(r.Context(), params.Get("code"), rp, codeOpts...) if err != nil { http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized) @@ -439,3 +460,13 @@ func WithCodeVerifier(codeVerifier string) CodeExchangeOpt { return []oauth2.AuthCodeOption{oauth2.SetAuthURLParam("code_verifier", codeVerifier)} } } + +//WithClientAssertionJWT sets the `client_assertion` param in the token request +func WithClientAssertionJWT(clientAssertion string) CodeExchangeOpt { + return func() []oauth2.AuthCodeOption { + return []oauth2.AuthCodeOption{ + oauth2.SetAuthURLParam("client_assertion", clientAssertion), + oauth2.SetAuthURLParam("client_assertion_type", oidc.ClientAssertionTypeJWTAssertion), + } + } +} diff --git a/pkg/rp/resource_server.go b/pkg/rp/resource_server.go new file mode 100644 index 0000000..e0488f1 --- /dev/null +++ b/pkg/rp/resource_server.go @@ -0,0 +1,118 @@ +package rp + +import ( + "context" + "errors" + "net/http" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" + "golang.org/x/oauth2/jwt" + + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/utils" +) + +type ResourceServer interface { + IntrospectionURL() string + HttpClient() *http.Client +} + +type resourceServer struct { + issuer string + tokenURL string + introspectURL string + httpClient *http.Client +} + +func (r *resourceServer) IntrospectionURL() string { + return r.introspectURL +} + +func (r *resourceServer) HttpClient() *http.Client { + return r.httpClient +} + +func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, option RSOption) (ResourceServer, error) { + authorizer := func(tokenURL string) func(ctx context.Context) *http.Client { + return (&clientcredentials.Config{ + ClientID: clientID, + ClientSecret: clientSecret, + TokenURL: tokenURL, + }).Client + } + return newResourceServer(issuer, authorizer, option) +} +func NewResourceServerJWTProfile(issuer, clientID, keyID string, key []byte, options ...RSOption) (ResourceServer, error) { + authorizer := func(tokenURL string) func(ctx context.Context) *http.Client { + return (&jwt.Config{ + Email: clientID, + Subject: clientID, + PrivateKey: key, + PrivateKeyID: keyID, + Audience: issuer, + TokenURL: tokenURL, + }).Client + } + return newResourceServer(issuer, authorizer, options...) +} + +func newResourceServer(issuer string, authorizer func(tokenURL string) func(ctx context.Context) *http.Client, options ...RSOption) (*resourceServer, error) { + rp := &resourceServer{ + issuer: issuer, + httpClient: utils.DefaultHTTPClient, + } + for _, optFunc := range options { + optFunc(rp) + } + if rp.introspectURL == "" || rp.tokenURL == "" { + endpoints, err := Discover(rp.issuer, rp.httpClient) + if err != nil { + return nil, err + } + rp.tokenURL = endpoints.TokenURL + rp.introspectURL = endpoints.IntrospectURL + } + if rp.introspectURL == "" || rp.tokenURL == "" { + return nil, errors.New("introspectURL and/or tokenURL is empty: please provide with either `WithStaticEndpoints` or a discovery url") + } + rp.httpClient = authorizer(rp.tokenURL)(context.WithValue(context.Background(), oauth2.HTTPClient, rp.HttpClient())) + return rp, nil +} + +func NewResourceServerFromKeyFile(path string, options ...RSOption) (ResourceServer, error) { + c, err := ConfigFromKeyFile(path) + if err != nil { + return nil, err + } + return NewResourceServerJWTProfile(c.Issuer, c.ClientID, c.KeyID, []byte(c.Key), options...) +} + +type RSOption func(*resourceServer) + +//WithClient provides the ability to set an http client to be used for the resource server +func WithClient(client *http.Client) RSOption { + return func(server *resourceServer) { + server.httpClient = client + } +} + +//WithStaticEndpoints provides the ability to set static token and introspect URL +func WithStaticEndpoints(tokenURL, introspectURL string) RSOption { + return func(server *resourceServer) { + server.tokenURL = tokenURL + server.introspectURL = introspectURL + } +} + +func Introspect(ctx context.Context, rp ResourceServer, token string) (oidc.IntrospectionResponse, error) { + req, err := utils.FormRequest(rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, encoder, nil) + if err != nil { + return nil, err + } + resp := oidc.NewIntrospectionResponse() + if err := utils.HttpRequest(rp.HttpClient(), req, resp); err != nil { + return nil, err + } + return resp, nil +} diff --git a/pkg/rp/tockenexchange.go b/pkg/rp/tockenexchange.go index 4396dc4..98bd841 100644 --- a/pkg/rp/tockenexchange.go +++ b/pkg/rp/tockenexchange.go @@ -43,8 +43,8 @@ func DelegationTokenExchange(ctx context.Context, subjectToken string, rp Relayi } //JWTProfileExchange handles the oauth2 jwt profile exchange -func JWTProfileExchange(ctx context.Context, jwtProfileRequest *tokenexchange.JWTProfileRequest, rp RelayingParty) (*oauth2.Token, error) { - return CallTokenEndpoint(jwtProfileRequest, rp) +func JWTProfileExchange(ctx context.Context, jwtProfileGrantRequest *oidc.JWTProfileGrantRequest, rp RelayingParty) (*oauth2.Token, error) { + return CallTokenEndpoint(jwtProfileGrantRequest, rp) } //JWTProfileExchange handles the oauth2 jwt profile exchange @@ -53,7 +53,7 @@ func JWTProfileAssertionExchange(ctx context.Context, assertion *oidc.JWTProfile if err != nil { return nil, err } - return JWTProfileExchange(ctx, tokenexchange.NewJWTProfileRequest(token, scopes...), rp) + return JWTProfileExchange(ctx, oidc.NewJWTProfileGrantRequest(token, scopes...), rp) } func generateJWTProfileToken(assertion *oidc.JWTProfileAssertion) (string, error) { From 4b426c899a4e29374f37b35430787d89dbb782c8 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Tue, 2 Feb 2021 11:41:50 +0100 Subject: [PATCH 06/14] scopes --- pkg/op/tokenrequest.go | 20 +++++++++++++++++++- pkg/rp/key.go | 15 +++++++++------ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/pkg/op/tokenrequest.go b/pkg/op/tokenrequest.go index 1345005..5cb872e 100644 --- a/pkg/op/tokenrequest.go +++ b/pkg/op/tokenrequest.go @@ -192,7 +192,7 @@ func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger JWTAuthorizati } //TODO: filter scopes - tokenRequest.Scopes = profileRequest.Scope + tokenRequest.Scopes = ValidateJWTProfileScopes(tokenRequest., profileRequest.Scope) resp, err := CreateJWTTokenResponse(r.Context(), tokenRequest, exchanger) if err != nil { @@ -215,6 +215,24 @@ func ParseJWTProfileRequest(r *http.Request, decoder utils.Decoder) (*oidc.JWTPr return tokenReq, nil } +func ValidateJWTProfileScopes(client Client, scopes []string) []string { + for i := len(scopes) - 1; i >= 0; i-- { + scope := scopes[i] + if !(scope == oidc.ScopeOpenID || + scope == oidc.ScopeProfile || + scope == oidc.ScopeEmail || + scope == oidc.ScopePhone || + scope == oidc.ScopeAddress || + scope == oidc.ScopeOfflineAccess) && //TODO: allowed + !client.IsScopeAllowed(scope) { + scopes[i] = scopes[len(scopes)-1] + scopes[len(scopes)-1] = "" + scopes = scopes[:len(scopes)-1] + } + } + return scopes +} + func TokenExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { tokenRequest, err := ParseTokenExchangeRequest(w, r) if err != nil { diff --git a/pkg/rp/key.go b/pkg/rp/key.go index 58a92d1..26d8bf5 100644 --- a/pkg/rp/key.go +++ b/pkg/rp/key.go @@ -11,13 +11,16 @@ const ( ) type keyFile struct { - Type string `json:"type"` // serviceaccount or application - KeyID string `json:"keyId"` - Key string `json:"key"` - Issuer string `json:"issuer"` + Type string `json:"type"` // serviceaccount or application + KeyID string `json:"keyId"` + Key string `json:"key"` + Issuer string `json:"issuer"` + + //serviceaccount + UserID string `json:"userId"` + + //application ClientID string `json:"clientId"` - //TokenURL string `json:"token_uri"` - //ProjectID string `json:"project_id"` } func ConfigFromKeyFile(path string) (*keyFile, error) { From 345fc7e837156fd1bdce8bcad423f8eb448b9395 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Wed, 3 Feb 2021 10:42:01 +0100 Subject: [PATCH 07/14] token introspection --- pkg/oidc/introspection.go | 11 +++++++---- pkg/op/storage.go | 3 ++- pkg/op/token_intospection.go | 23 ++++++++++++++--------- pkg/op/tokenrequest.go | 26 +++++--------------------- 4 files changed, 28 insertions(+), 35 deletions(-) diff --git a/pkg/oidc/introspection.go b/pkg/oidc/introspection.go index 98f3969..98a8e25 100644 --- a/pkg/oidc/introspection.go +++ b/pkg/oidc/introspection.go @@ -6,8 +6,6 @@ import ( "time" "golang.org/x/text/language" - - "github.com/caos/oidc/pkg/utils" ) type IntrospectionRequest struct { @@ -230,11 +228,16 @@ func (i *introspectionResponse) MarshalJSON() ([]byte, error) { return b, nil } - claims, err := json.Marshal(i.claims) + err = json.Unmarshal(b, &i.claims) if err != nil { return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims) } - return utils.ConcatenateJSON(b, claims) + + return json.Marshal(i.claims) + //if err != nil { + // return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims) + //} + //return utils.ConcatenateJSON(b, claims) } func (i *introspectionResponse) UnmarshalJSON(data []byte) error { diff --git a/pkg/op/storage.go b/pkg/op/storage.go index c4da464..4072630 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -30,9 +30,10 @@ type OPStorage interface { AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error + SetIntrospectionFromToken(ctx context.Context, userinfo oidc.IntrospectionResponse, tokenID, subject, callerTokenID, callerSubject string) error GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]interface{}, error) GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) - //ValidateJWTProfileScopes(ctx context.Context, userID string, scope oidc.Scopes) (oidc.Scopes, error) + ValidateJWTProfileScopes(ctx context.Context, userID string, scope oidc.Scopes) (oidc.Scopes, error) //deprecated: use GetUserinfoFromScopes instead GetUserinfoFromScopes(ctx context.Context, userID, clientID string, scopes []string) (oidc.UserInfo, error) diff --git a/pkg/op/token_intospection.go b/pkg/op/token_intospection.go index e6a5d4a..8ea4dbb 100644 --- a/pkg/op/token_intospection.go +++ b/pkg/op/token_intospection.go @@ -3,6 +3,7 @@ package op import ( "errors" "net/http" + "strings" "github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/utils" @@ -22,20 +23,24 @@ func introspectionHandler(introspector Introspector) func(http.ResponseWriter, * } func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspector) { - //validate authorization - + callerToken := r.Header.Get("authorization") response := oidc.NewIntrospectionResponse() - token, err := ParseTokenInrospectionRequest(r, introspector.Decoder()) - if err != nil { - utils.MarshalJSON(w, response) - return - } - tokenID, subject, ok := getTokenIDAndSubject(r.Context(), introspector, token) + callerToken, callerSubject, ok := getTokenIDAndSubject(r.Context(), introspector, strings.TrimPrefix(callerToken, oidc.PrefixBearer)) if !ok { utils.MarshalJSON(w, response) return } - err = introspector.Storage().SetUserinfoFromToken(r.Context(), response, tokenID, subject, r.Header.Get("origin")) + introspectionToken, err := ParseTokenInrospectionRequest(r, introspector.Decoder()) + if err != nil { + utils.MarshalJSON(w, response) + return + } + tokenID, subject, ok := getTokenIDAndSubject(r.Context(), introspector, introspectionToken) + if !ok { + utils.MarshalJSON(w, response) + return + } + err = introspector.Storage().SetIntrospectionFromToken(r.Context(), response, tokenID, subject, callerToken, callerSubject) if err != nil { utils.MarshalJSON(w, response) return diff --git a/pkg/op/tokenrequest.go b/pkg/op/tokenrequest.go index 5cb872e..e0729bf 100644 --- a/pkg/op/tokenrequest.go +++ b/pkg/op/tokenrequest.go @@ -191,9 +191,11 @@ func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger JWTAuthorizati return } - //TODO: filter scopes - tokenRequest.Scopes = ValidateJWTProfileScopes(tokenRequest., profileRequest.Scope) - + tokenRequest.Scopes, err = exchanger.Storage().ValidateJWTProfileScopes(r.Context(), tokenRequest.Issuer, profileRequest.Scope) + if err != nil { + RequestError(w, r, err) + return + } resp, err := CreateJWTTokenResponse(r.Context(), tokenRequest, exchanger) if err != nil { RequestError(w, r, err) @@ -215,24 +217,6 @@ func ParseJWTProfileRequest(r *http.Request, decoder utils.Decoder) (*oidc.JWTPr return tokenReq, nil } -func ValidateJWTProfileScopes(client Client, scopes []string) []string { - for i := len(scopes) - 1; i >= 0; i-- { - scope := scopes[i] - if !(scope == oidc.ScopeOpenID || - scope == oidc.ScopeProfile || - scope == oidc.ScopeEmail || - scope == oidc.ScopePhone || - scope == oidc.ScopeAddress || - scope == oidc.ScopeOfflineAccess) && //TODO: allowed - !client.IsScopeAllowed(scope) { - scopes[i] = scopes[len(scopes)-1] - scopes[len(scopes)-1] = "" - scopes = scopes[:len(scopes)-1] - } - } - return scopes -} - func TokenExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { tokenRequest, err := ParseTokenExchangeRequest(w, r) if err != nil { From 138da8a208ed248452b77ed114ddac348f1996c5 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Wed, 10 Feb 2021 16:42:01 +0100 Subject: [PATCH 08/14] introspect --- example/client/app/app.go | 18 +++++-- pkg/oidc/discovery.go | 67 ++++++++++++++++++------- pkg/oidc/introspection.go | 26 ++++++++-- pkg/oidc/token.go | 4 +- pkg/oidc/types.go | 14 ++++++ pkg/oidc/userinfo.go | 16 ++++-- pkg/op/discovery.go | 57 ++++++++++++++++------ pkg/op/storage.go | 2 +- pkg/op/token_intospection.go | 50 ++++++++++++------- pkg/op/userinfo.go | 28 +++++++---- pkg/rp/relaying_party.go | 24 ++++++--- pkg/rp/resource_server.go | 94 ++++++++++++++++++++++++++++++------ pkg/utils/http.go | 3 ++ 13 files changed, 305 insertions(+), 98 deletions(-) diff --git a/example/client/app/app.go b/example/client/app/app.go index ad00adb..c3bf9e0 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -27,6 +27,7 @@ var ( func main() { clientID := os.Getenv("CLIENT_ID") clientSecret := os.Getenv("CLIENT_SECRET") + keyPath := os.Getenv("KEY_PATH") issuer := os.Getenv("ISSUER") port := os.Getenv("PORT") scopes := strings.Split(os.Getenv("SCOPES"), " ") @@ -35,10 +36,19 @@ func main() { redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath) cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure()) - provider, err := rp.NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes, - rp.WithPKCE(cookieHandler), - rp.WithVerifierOpts(rp.WithIssuedAtOffset(5*time.Second)), - ) + + options := []rp.Option{ + rp.WithCookieHandler(cookieHandler), + rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)), + } + if clientSecret == "" { + options = append(options, rp.WithPKCE(cookieHandler)) + } + if keyPath != "" { + options = append(options, rp.WithClientKey(keyPath)) + } + + provider, err := rp.NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes, options...) if err != nil { logrus.Fatalf("error creating provider %s", err.Error()) } diff --git a/pkg/oidc/discovery.go b/pkg/oidc/discovery.go index 970369c..104078c 100644 --- a/pkg/oidc/discovery.go +++ b/pkg/oidc/discovery.go @@ -1,27 +1,56 @@ package oidc +import ( + "golang.org/x/text/language" +) + const ( DiscoveryEndpoint = "/.well-known/openid-configuration" ) type DiscoveryConfiguration struct { - Issuer string `json:"issuer,omitempty"` - AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"` - TokenEndpoint string `json:"token_endpoint,omitempty"` - IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"` - UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"` - EndSessionEndpoint string `json:"end_session_endpoint,omitempty"` - CheckSessionIframe string `json:"check_session_iframe,omitempty"` - JwksURI string `json:"jwks_uri,omitempty"` - ScopesSupported []string `json:"scopes_supported,omitempty"` - ResponseTypesSupported []string `json:"response_types_supported,omitempty"` - ResponseModesSupported []string `json:"response_modes_supported,omitempty"` - GrantTypesSupported []string `json:"grant_types_supported,omitempty"` - SubjectTypesSupported []string `json:"subject_types_supported,omitempty"` - IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"` - TokenEndpointAuthMethodsSupported []AuthMethod `json:"token_endpoint_auth_methods_supported,omitempty"` - CodeChallengeMethodsSupported []CodeChallengeMethod `json:"code_challenge_methods_supported,omitempty"` - ClaimsSupported []string `json:"claims_supported,omitempty"` + Issuer string `json:"issuer,omitempty"` + AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"` + TokenEndpoint string `json:"token_endpoint,omitempty"` + IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"` + UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"` + RevocationEndpoint string `json:"revocation_endpoint,omitempty"` + EndSessionEndpoint string `json:"end_session_endpoint,omitempty"` + CheckSessionIframe string `json:"check_session_iframe,omitempty"` + JwksURI string `json:"jwks_uri,omitempty"` + ScopesSupported []string `json:"scopes_supported,omitempty"` + ResponseTypesSupported []string `json:"response_types_supported"` + ResponseModesSupported []string `json:"response_modes_supported,omitempty"` + GrantTypesSupported []GrantType `json:"grant_types_supported,omitempty"` + ACRValuesSupported []string `json:"acr_values_supported,omitempty"` + SubjectTypesSupported []string `json:"subject_types_supported,omitempty"` + IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"` + IDTokenEncryptionAlgValuesSupported []string `json:"id_token_encryption_alg_values_supported,omitempty"` + IDTokenEncryptionEncValuesSupported []string `json:"id_token_encryption_enc_values_supported,omitempty"` + UserinfoSigningAlgValuesSupported []string `json:"userinfo_signing_alg_values_supported,omitempty"` + UserinfoEncryptionAlgValuesSupported []string `json:"userinfo_encryption_alg_values_supported,omitempty"` + UserinfoEncryptionEncValuesSupported []string `json:"userinfo_encryption_enc_values_supported,omitempty"` + RequestObjectSigningAlgValuesSupported []string `json:"request_object_signing_alg_values_supported,omitempty"` + RequestObjectEncryptionAlgValuesSupported []string `json:"request_object_encryption_alg_values_supported,omitempty"` + RequestObjectEncryptionEncValuesSupported []string `json:"request_object_encryption_enc_values_supported,omitempty"` + TokenEndpointAuthMethodsSupported []AuthMethod `json:"token_endpoint_auth_methods_supported,omitempty"` + TokenEndpointAuthSigningAlgValuesSupported []string `json:"token_endpoint_auth_signing_alg_values_supported,omitempty"` + RevocationEndpointAuthMethodsSupported []AuthMethod `json:"revocation_endpoint_auth_methods_supported,omitempty"` + RevocationEndpointAuthSigningAlgValuesSupported []string `json:"revocation_endpoint_auth_signing_alg_values_supported,omitempty"` + IntrospectionEndpointAuthMethodsSupported []AuthMethod `json:"introspection_endpoint_auth_methods_supported,omitempty"` + IntrospectionEndpointAuthSigningAlgValuesSupported []string `json:"introspection_endpoint_auth_signing_alg_values_supported,omitempty"` + DisplayValuesSupported []Display `json:"display_values_supported,omitempty"` + ClaimTypesSupported []string `json:"claim_types_supported,omitempty"` + ClaimsSupported []string `json:"claims_supported,omitempty"` + CodeChallengeMethodsSupported []CodeChallengeMethod `json:"code_challenge_methods_supported,omitempty"` + ServiceDocumentation string `json:"service_documentation,omitempty"` + ClaimsLocalesSupported []language.Tag `json:"claims_locales_supported,omitempty"` + UILocalesSupported []language.Tag `json:"ui_locales_supported,omitempty"` + RequestParameterSupported bool `json:"request_parameter_supported,omitempty"` + RequestURIParameterSupported bool `json:"request_uri_parameter_supported"` //no omitempty because: If omitted, the default value is true + RequireRequestURIRegistration bool `json:"require_request_uri_registration,omitempty"` + OPPolicyURI string `json:"op_policy_uri,omitempty"` + OPTermsOfServiceURI string `json:"op_tos_uri,omitempty"` } type AuthMethod string @@ -32,3 +61,7 @@ const ( AuthMethodNone AuthMethod = "none" AuthMethodPrivateKeyJWT AuthMethod = "private_key_jwt" ) + +const ( + GrantTypeImplicit GrantType = "implicit" +) diff --git a/pkg/oidc/introspection.go b/pkg/oidc/introspection.go index 98a8e25..1a66520 100644 --- a/pkg/oidc/introspection.go +++ b/pkg/oidc/introspection.go @@ -12,10 +12,17 @@ type IntrospectionRequest struct { Token string `schema:"token"` } +type ClientAssertionParams struct { + ClientAssertion string `schema:"client_assertion"` + ClientAssertionType string `schema:"client_assertion_type"` +} + type IntrospectionResponse interface { UserInfoSetter SetActive(bool) IsActive() bool + SetScopes(scopes Scope) + SetClientID(id string) } func NewIntrospectionResponse() IntrospectionResponse { @@ -23,19 +30,30 @@ func NewIntrospectionResponse() IntrospectionResponse { } type introspectionResponse struct { - Active bool `json:"active"` - Subject string `json:"sub,omitempty"` + Active bool `json:"active"` + Scope Scope `json:"scope,omitempty"` + ClientID string `json:"client_id,omitempty"` + Subject string `json:"sub,omitempty"` userInfoProfile userInfoEmail userInfoPhone - Address UserInfoAddress `json:"address,omitempty"` - claims map[string]interface{} + Address UserInfoAddress `json:"address,omitempty"` + claims map[string]interface{} } func (u *introspectionResponse) IsActive() bool { return u.Active } + +func (u *introspectionResponse) SetScopes(scope Scope) { + u.Scope = scope +} + +func (u *introspectionResponse) SetClientID(id string) { + u.ClientID = id +} + func (u *introspectionResponse) GetSubject() string { return u.Subject } diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index 1fc40b3..068e8e6 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -427,7 +427,7 @@ func NewJWTProfileAssertionStringFromFileData(data []byte, audience []string) (s if err != nil { return "", err } - return generateJWTProfileToken(NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key))) + return GenerateJWTProfileToken(NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key))) } func NewJWTProfileAssertionFromFileData(data []byte, audience []string) (*JWTProfileAssertion, error) { @@ -473,7 +473,7 @@ func AppendClientIDToAudience(clientID string, audience []string) []string { return append(audience, clientID) } -func generateJWTProfileToken(assertion *JWTProfileAssertion) (string, error) { +func GenerateJWTProfileToken(assertion *JWTProfileAssertion) (string, error) { privateKey, err := bytesToPrivateKey(assertion.PrivateKey) if err != nil { return "", err diff --git a/pkg/oidc/types.go b/pkg/oidc/types.go index 86e5d06..fd496da 100644 --- a/pkg/oidc/types.go +++ b/pkg/oidc/types.go @@ -59,6 +59,7 @@ type Prompt string type ResponseType string type Scopes []string +type Scope []string //TODO: hurst? func (s Scopes) Encode() string { return strings.Join(s, " ") @@ -73,6 +74,19 @@ func (s *Scopes) MarshalText() ([]byte, error) { return []byte(s.Encode()), nil } +func (s *Scope) MarshalJSON() ([]byte, error) { + return json.Marshal(Scopes(*s).Encode()) +} + +func (s *Scope) UnmarshalJSON(data []byte) error { + var str string + if err := json.Unmarshal(data, &str); err != nil { + return err + } + *s = Scope(strings.Split(str, " ")) + return nil +} + type Time time.Time func (t *Time) UnmarshalJSON(data []byte) error { diff --git a/pkg/oidc/userinfo.go b/pkg/oidc/userinfo.go index 3c77b7b..3a92501 100644 --- a/pkg/oidc/userinfo.go +++ b/pkg/oidc/userinfo.go @@ -6,8 +6,6 @@ import ( "time" "golang.org/x/text/language" - - "github.com/caos/oidc/pkg/utils" ) type UserInfo interface { @@ -351,11 +349,17 @@ func (i *userinfo) MarshalJSON() ([]byte, error) { return b, nil } - claims, err := json.Marshal(i.claims) + err = json.Unmarshal(b, &i.claims) if err != nil { return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims) } - return utils.ConcatenateJSON(b, claims) + + return json.Marshal(i.claims) + //claims, err := json.Marshal(i.claims) + //if err != nil { + // return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims) + //} + //return utils.ConcatenateJSON(b, claims) } func (i *userinfo) UnmarshalJSON(data []byte) error { @@ -372,6 +376,10 @@ func (i *userinfo) UnmarshalJSON(data []byte) error { i.UpdatedAt = Time(time.Unix(a.UpdatedAt, 0).UTC()) + if err := json.Unmarshal(data, &i.claims); err != nil { + return err + } + return nil } diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 2ac4833..291214c 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -3,6 +3,8 @@ package op import ( "net/http" + "golang.org/x/text/language" + "github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/utils" ) @@ -24,17 +26,22 @@ func CreateDiscoveryConfig(c Configuration, s Signer) *oidc.DiscoveryConfigurati TokenEndpoint: c.TokenEndpoint().Absolute(c.Issuer()), IntrospectionEndpoint: c.IntrospectionEndpoint().Absolute(c.Issuer()), UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()), - EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()), + //RevocationEndpoint: c.RevocationEndpoint().Absolute(c.Issuer()), + EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()), // CheckSessionIframe: c.TokenEndpoint().Absolute(c.Issuer())(c.CheckSessionIframe), - JwksURI: c.KeysEndpoint().Absolute(c.Issuer()), - ScopesSupported: Scopes(c), - ResponseTypesSupported: ResponseTypes(c), - GrantTypesSupported: GrantTypes(c), - ClaimsSupported: SupportedClaims(c), - IDTokenSigningAlgValuesSupported: SigAlgorithms(s), - SubjectTypesSupported: SubjectTypes(c), - TokenEndpointAuthMethodsSupported: AuthMethods(c), - CodeChallengeMethodsSupported: CodeChallengeMethods(c), + JwksURI: c.KeysEndpoint().Absolute(c.Issuer()), + ScopesSupported: Scopes(c), + ResponseTypesSupported: ResponseTypes(c), + //ResponseModesSupported: + GrantTypesSupported: GrantTypes(c), + //ACRValuesSupported: ACRValues(c), + SubjectTypesSupported: SubjectTypes(c), + IDTokenSigningAlgValuesSupported: SigAlgorithms(s), + TokenEndpointAuthMethodsSupported: AuthMethodsTokenEndpoint(c), + IntrospectionEndpointAuthMethodsSupported: AuthMethodsIntrospectionEndpoint(c), + ClaimsSupported: SupportedClaims(c), + CodeChallengeMethodsSupported: CodeChallengeMethods(c), + UILocalesSupported: UILocales(c), } } @@ -58,15 +65,16 @@ func ResponseTypes(c Configuration) []string { } //TODO: ok for now, check later if dynamic needed } -func GrantTypes(c Configuration) []string { - grantTypes := []string{ - string(oidc.GrantTypeCode), +func GrantTypes(c Configuration) []oidc.GrantType { + grantTypes := []oidc.GrantType{ + oidc.GrantTypeCode, + oidc.GrantTypeImplicit, } if c.GrantTypeTokenExchangeSupported() { - grantTypes = append(grantTypes, string(oidc.GrantTypeTokenExchange)) + grantTypes = append(grantTypes, oidc.GrantTypeTokenExchange) } if c.GrantTypeJWTAuthorizationSupported() { - grantTypes = append(grantTypes, string(oidc.GrantTypeBearer)) + grantTypes = append(grantTypes, oidc.GrantTypeBearer) } return grantTypes } @@ -108,7 +116,7 @@ func SubjectTypes(c Configuration) []string { return []string{"public"} //TODO: config } -func AuthMethods(c Configuration) []oidc.AuthMethod { +func AuthMethodsTokenEndpoint(c Configuration) []oidc.AuthMethod { authMethods := []oidc.AuthMethod{ oidc.AuthMethodNone, oidc.AuthMethodBasic, @@ -122,6 +130,16 @@ func AuthMethods(c Configuration) []oidc.AuthMethod { return authMethods } +func AuthMethodsIntrospectionEndpoint(c Configuration) []oidc.AuthMethod { + authMethods := []oidc.AuthMethod{ + oidc.AuthMethodBasic, + } + if c.AuthMethodPrivateKeyJWTSupported() { + authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT) + } + return authMethods +} + func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod { codeMethods := make([]oidc.CodeChallengeMethod, 0, 1) if c.CodeMethodS256Supported() { @@ -129,3 +147,10 @@ func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod { } return codeMethods } + +func UILocales(c Configuration) []language.Tag { + return []language.Tag{ + language.English, + language.German, + } +} diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 4072630..967a24c 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -30,7 +30,7 @@ type OPStorage interface { AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error - SetIntrospectionFromToken(ctx context.Context, userinfo oidc.IntrospectionResponse, tokenID, subject, callerTokenID, callerSubject string) error + SetIntrospectionFromToken(ctx context.Context, userinfo oidc.IntrospectionResponse, tokenID, subject, clientID string) error GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]interface{}, error) GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) ValidateJWTProfileScopes(ctx context.Context, userID string, scope oidc.Scopes) (oidc.Scopes, error) diff --git a/pkg/op/token_intospection.go b/pkg/op/token_intospection.go index 8ea4dbb..30d2544 100644 --- a/pkg/op/token_intospection.go +++ b/pkg/op/token_intospection.go @@ -3,7 +3,6 @@ package op import ( "errors" "net/http" - "strings" "github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/utils" @@ -16,6 +15,11 @@ type Introspector interface { AccessTokenVerifier() AccessTokenVerifier } +type IntrospectorJWTProfile interface { + Introspector + JWTProfileVerifier() JWTProfileVerifier +} + func introspectionHandler(introspector Introspector) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { Introspect(w, r, introspector) @@ -23,24 +27,18 @@ func introspectionHandler(introspector Introspector) func(http.ResponseWriter, * } func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspector) { - callerToken := r.Header.Get("authorization") response := oidc.NewIntrospectionResponse() - callerToken, callerSubject, ok := getTokenIDAndSubject(r.Context(), introspector, strings.TrimPrefix(callerToken, oidc.PrefixBearer)) - if !ok { - utils.MarshalJSON(w, response) - return - } - introspectionToken, err := ParseTokenInrospectionRequest(r, introspector.Decoder()) + token, clientID, err := ParseTokenIntrospectionRequest(r, introspector) if err != nil { - utils.MarshalJSON(w, response) + http.Error(w, err.Error(), http.StatusUnauthorized) return } - tokenID, subject, ok := getTokenIDAndSubject(r.Context(), introspector, introspectionToken) + tokenID, subject, ok := getTokenIDAndSubject(r.Context(), introspector, token) if !ok { utils.MarshalJSON(w, response) return } - err = introspector.Storage().SetIntrospectionFromToken(r.Context(), response, tokenID, subject, callerToken, callerSubject) + err = introspector.Storage().SetIntrospectionFromToken(r.Context(), response, tokenID, subject, clientID) if err != nil { utils.MarshalJSON(w, response) return @@ -49,15 +47,31 @@ func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspecto utils.MarshalJSON(w, response) } -func ParseTokenInrospectionRequest(r *http.Request, decoder utils.Decoder) (string, error) { - err := r.ParseForm() +func ParseTokenIntrospectionRequest(r *http.Request, introspector Introspector) (token, clientID string, err error) { + err = r.ParseForm() if err != nil { - return "", errors.New("unable to parse request") + return "", "", errors.New("unable to parse request") } - req := new(oidc.IntrospectionRequest) - err = decoder.Decode(req, r.Form) + req := new(struct { + oidc.IntrospectionRequest + oidc.ClientAssertionParams + }) + err = introspector.Decoder().Decode(req, r.Form) if err != nil { - return "", errors.New("unable to parse request") + return "", "", errors.New("unable to parse request") } - return req.Token, nil + if introspectorJWTProfile, ok := introspector.(IntrospectorJWTProfile); ok && req.ClientAssertion != "" { + profile, err := VerifyJWTAssertion(r.Context(), req.ClientAssertion, introspectorJWTProfile.JWTProfileVerifier()) + if err == nil { + return req.Token, profile.Issuer, nil + } + } + clientID, clientSecret, ok := r.BasicAuth() + if ok { + if err := introspector.Storage().AuthorizeClientIDSecret(r.Context(), clientID, clientSecret); err != nil { + return "", "", err + } + return req.Token, clientID, nil + } + return "", "", errors.New("invalid authorization") } diff --git a/pkg/op/userinfo.go b/pkg/op/userinfo.go index 1163598..d951136 100644 --- a/pkg/op/userinfo.go +++ b/pkg/op/userinfo.go @@ -24,7 +24,7 @@ func userinfoHandler(userinfoProvider UserinfoProvider) func(http.ResponseWriter } func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoProvider) { - accessToken, err := getAccessToken(r, userinfoProvider.Decoder()) + accessToken, err := ParseUserinfoRequest(r, userinfoProvider.Decoder()) if err != nil { http.Error(w, "access token missing", http.StatusUnauthorized) return @@ -43,16 +43,12 @@ func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoP utils.MarshalJSON(w, info) } -func getAccessToken(r *http.Request, decoder utils.Decoder) (string, error) { - authHeader := r.Header.Get("authorization") - if authHeader != "" { - parts := strings.Split(authHeader, "Bearer ") - if len(parts) != 2 { - return "", errors.New("invalid auth header") - } - return parts[1], nil +func ParseUserinfoRequest(r *http.Request, decoder utils.Decoder) (string, error) { + accessToken, err := getAccessToken(r) + if err == nil { + return accessToken, nil } - err := r.ParseForm() + err = r.ParseForm() if err != nil { return "", errors.New("unable to parse request") } @@ -64,6 +60,18 @@ func getAccessToken(r *http.Request, decoder utils.Decoder) (string, error) { return req.AccessToken, nil } +func getAccessToken(r *http.Request) (string, error) { + authHeader := r.Header.Get("authorization") + if authHeader == "" { + return "", errors.New("no auth header") + } + parts := strings.Split(authHeader, "Bearer ") + if len(parts) != 2 { + return "", errors.New("invalid auth header") + } + return parts[1], nil +} + func getTokenIDAndSubject(ctx context.Context, userinfoProvider UserinfoProvider, accessToken string) (string, string, bool) { tokenIDSubject, err := userinfoProvider.Crypto().Decrypt(accessToken) if err == nil { diff --git a/pkg/rp/relaying_party.go b/pkg/rp/relaying_party.go index be62762..3260657 100644 --- a/pkg/rp/relaying_party.go +++ b/pkg/rp/relaying_party.go @@ -216,6 +216,14 @@ func WithVerifierOpts(opts ...VerifierOption) Option { } } +func WithClientKey(path string) Option { + return func(rp *relayingParty) { + config, _ := ConfigFromKeyFile(path) + rp.clientKey = []byte(config.Key) + rp.clientKeyID = config.KeyID + } +} + //Discover calls the discovery endpoint of the provided issuer and returns the found endpoints func Discover(issuer string, httpClient *http.Client) (Endpoints, error) { wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint @@ -327,14 +335,14 @@ func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc } codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier)) } - //if len(rp.ClientKey()) > 0 { - // assertion, err := oidc.NewJWTProfileAssertionStringFromFileData(rp.ClientKey(), []string{rp.OAuthConfig().Endpoint.TokenURL}) - // if err != nil { - // http.Error(w, "failed to build assertion: "+err.Error(), http.StatusUnauthorized) - // return - // } - // codeOpts = append(codeOpts, WithClientAssertionJWT(assertion)) - //} + if len(rp.ClientKey()) > 0 { + assertion, err := oidc.GenerateJWTProfileToken(oidc.NewJWTProfileAssertion(rp.OAuthConfig().ClientID, rp.ClientKeyID(), []string{"http://localhost:50002/oauth/v2"}, rp.ClientKey())) + if err != nil { + http.Error(w, "failed to build assertion: "+err.Error(), http.StatusUnauthorized) + return + } + codeOpts = append(codeOpts, WithClientAssertionJWT(assertion)) + } tokens, err := CodeExchange(r.Context(), params.Get("code"), rp, codeOpts...) if err != nil { http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized) diff --git a/pkg/rp/resource_server.go b/pkg/rp/resource_server.go index e0488f1..c59097d 100644 --- a/pkg/rp/resource_server.go +++ b/pkg/rp/resource_server.go @@ -4,10 +4,11 @@ import ( "context" "errors" "net/http" + "net/url" + "time" "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" - "golang.org/x/oauth2/jwt" "github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/utils" @@ -16,6 +17,7 @@ import ( type ResourceServer interface { IntrospectionURL() string HttpClient() *http.Client + AuthFn() interface{} } type resourceServer struct { @@ -23,6 +25,32 @@ type resourceServer struct { tokenURL string introspectURL string httpClient *http.Client + authFn interface{} +} + +type jwtAccessTokenSource struct { + clientID string + audience []string + PrivateKey []byte + PrivateKeyID string +} + +func (j *jwtAccessTokenSource) Token() (*oauth2.Token, error) { + iat := time.Now() + exp := iat.Add(time.Hour) + assertion, err := GenerateJWTProfileToken(&oidc.JWTProfileAssertion{ + PrivateKeyID: j.PrivateKeyID, + PrivateKey: j.PrivateKey, + Issuer: j.clientID, + Subject: j.clientID, + Audience: j.audience, + Expiration: oidc.Time(exp), + IssuedAt: oidc.Time(iat), + }) + if err != nil { + return nil, err + } + return &oauth2.Token{AccessToken: assertion, TokenType: "Bearer", Expiry: exp}, nil } func (r *resourceServer) IntrospectionURL() string { @@ -33,8 +61,12 @@ func (r *resourceServer) HttpClient() *http.Client { return r.httpClient } +func (r *resourceServer) AuthFn() interface{} { + return r.authFn +} + func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, option RSOption) (ResourceServer, error) { - authorizer := func(tokenURL string) func(ctx context.Context) *http.Client { + authorizer := func(tokenURL string) func(context.Context) *http.Client { return (&clientcredentials.Config{ ClientID: clientID, ClientSecret: clientSecret, @@ -44,20 +76,52 @@ func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, o return newResourceServer(issuer, authorizer, option) } func NewResourceServerJWTProfile(issuer, clientID, keyID string, key []byte, options ...RSOption) (ResourceServer, error) { - authorizer := func(tokenURL string) func(ctx context.Context) *http.Client { - return (&jwt.Config{ - Email: clientID, - Subject: clientID, - PrivateKey: key, - PrivateKeyID: keyID, - Audience: issuer, - TokenURL: tokenURL, - }).Client + ts := &jwtAccessTokenSource{ + clientID: clientID, + PrivateKey: key, + PrivateKeyID: keyID, + audience: []string{issuer}, } + + //authorizer := func(tokenURL string) func(context.Context) *http.Client { + // return func(ctx context.Context) *http.Client { + // return oauth2.NewClient(ctx, oauth2.ReuseTokenSource(token, ts)) + // } + //} + authorizer := utils.FormAuthorization(func(values url.Values) { + token, err := ts.Token() + if err != nil { + //return nil, err + } + values.Set("client_assertion", token.AccessToken) + }) return newResourceServer(issuer, authorizer, options...) } -func newResourceServer(issuer string, authorizer func(tokenURL string) func(ctx context.Context) *http.Client, options ...RSOption) (*resourceServer, error) { +// +//func newResourceServer(issuer string, authorizer func(tokenURL string) func(ctx context.Context) *http.Client, options ...RSOption) (*resourceServer, error) { +// rp := &resourceServer{ +// issuer: issuer, +// httpClient: utils.DefaultHTTPClient, +// } +// for _, optFunc := range options { +// optFunc(rp) +// } +// if rp.introspectURL == "" || rp.tokenURL == "" { +// endpoints, err := Discover(rp.issuer, rp.httpClient) +// if err != nil { +// return nil, err +// } +// rp.tokenURL = endpoints.TokenURL +// rp.introspectURL = endpoints.IntrospectURL +// } +// if rp.introspectURL == "" || rp.tokenURL == "" { +// return nil, errors.New("introspectURL and/or tokenURL is empty: please provide with either `WithStaticEndpoints` or a discovery url") +// } +// //rp.httpClient = authorizer(rp.tokenURL)(context.WithValue(context.Background(), oauth2.HTTPClient, rp.HttpClient())) +// return rp, nil +//} +func newResourceServer(issuer string, authorizer interface{}, options ...RSOption) (*resourceServer, error) { rp := &resourceServer{ issuer: issuer, httpClient: utils.DefaultHTTPClient, @@ -76,7 +140,8 @@ func newResourceServer(issuer string, authorizer func(tokenURL string) func(ctx if rp.introspectURL == "" || rp.tokenURL == "" { return nil, errors.New("introspectURL and/or tokenURL is empty: please provide with either `WithStaticEndpoints` or a discovery url") } - rp.httpClient = authorizer(rp.tokenURL)(context.WithValue(context.Background(), oauth2.HTTPClient, rp.HttpClient())) + //rp.httpClient = authorizer(rp.tokenURL)(context.WithValue(context.Background(), oauth2.HTTPClient, rp.HttpClient())) + rp.authFn = authorizer return rp, nil } @@ -85,6 +150,7 @@ func NewResourceServerFromKeyFile(path string, options ...RSOption) (ResourceSer if err != nil { return nil, err } + c.Issuer = "http://localhost:50002/oauth/v2" return NewResourceServerJWTProfile(c.Issuer, c.ClientID, c.KeyID, []byte(c.Key), options...) } @@ -106,7 +172,7 @@ func WithStaticEndpoints(tokenURL, introspectURL string) RSOption { } func Introspect(ctx context.Context, rp ResourceServer, token string) (oidc.IntrospectionResponse, error) { - req, err := utils.FormRequest(rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, encoder, nil) + req, err := utils.FormRequest(rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, encoder, rp.AuthFn()) if err != nil { return nil, err } diff --git a/pkg/utils/http.go b/pkg/utils/http.go index fa51815..6f1b74d 100644 --- a/pkg/utils/http.go +++ b/pkg/utils/http.go @@ -42,6 +42,9 @@ func FormRequest(endpoint string, request interface{}, encoder Encoder, authFn i if fn, ok := authFn.(FormAuthorization); ok { fn(form) } + if fn, ok := authFn.(func(url.Values)); ok { + fn(form) + } body := strings.NewReader(form.Encode()) req, err := http.NewRequest("POST", endpoint, body) if err != nil { From 0ca2370d48b15b175141155f37673095de1729a9 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Thu, 11 Feb 2021 17:38:58 +0100 Subject: [PATCH 09/14] refactoring --- example/client/api/api.go | 9 +- example/client/app/app.go | 83 +-------- example/client/github/github.go | 10 +- example/client/service/service.go | 196 ++++++++++++++++++++++ pkg/client/client.go | 90 ++++++++++ pkg/client/jwt_profile.go | 30 ++++ pkg/{rp => client}/key.go | 8 +- pkg/client/profile/jwt_profile.go | 85 ++++++++++ pkg/{ => client}/rp/cli/cli.go | 8 +- pkg/{ => client}/rp/delegation.go | 0 pkg/{ => client}/rp/jwks.go | 0 pkg/{ => client}/rp/mock/generate.go | 0 pkg/{ => client}/rp/mock/verifier.mock.go | 10 +- pkg/{ => client}/rp/relaying_party.go | 196 ++++++++++------------ pkg/client/rp/tockenexchange.go | 27 +++ pkg/{ => client}/rp/verifier.go | 10 -- pkg/client/rs/resource_server.go | 123 ++++++++++++++ pkg/oidc/discovery.go | 1 + pkg/op/storage.go | 5 - pkg/op/token.go | 3 +- pkg/op/tokenrequest.go | 3 + pkg/op/userinfo.go | 3 +- pkg/rp/resource_server.go | 184 -------------------- pkg/rp/tockenexchange.go | 100 ----------- pkg/utils/key.go | 25 +++ 25 files changed, 698 insertions(+), 511 deletions(-) create mode 100644 example/client/service/service.go create mode 100644 pkg/client/client.go create mode 100644 pkg/client/jwt_profile.go rename pkg/{rp => client}/key.go (78%) create mode 100644 pkg/client/profile/jwt_profile.go rename pkg/{ => client}/rp/cli/cli.go (67%) rename pkg/{ => client}/rp/delegation.go (100%) rename pkg/{ => client}/rp/jwks.go (100%) rename pkg/{ => client}/rp/mock/generate.go (100%) rename pkg/{ => client}/rp/mock/verifier.mock.go (94%) rename pkg/{ => client}/rp/relaying_party.go (69%) create mode 100644 pkg/client/rp/tockenexchange.go rename pkg/{ => client}/rp/verifier.go (92%) create mode 100644 pkg/client/rs/resource_server.go delete mode 100644 pkg/rp/resource_server.go delete mode 100644 pkg/rp/tockenexchange.go create mode 100644 pkg/utils/key.go diff --git a/example/client/api/api.go b/example/client/api/api.go index e5345a8..a3ae85e 100644 --- a/example/client/api/api.go +++ b/example/client/api/api.go @@ -12,8 +12,8 @@ import ( "github.com/gorilla/mux" "github.com/sirupsen/logrus" + "github.com/caos/oidc/pkg/client/rs" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/rp" ) const ( @@ -25,8 +25,9 @@ const ( func main() { keyPath := os.Getenv("KEY") port := os.Getenv("PORT") + issuer := os.Getenv("ISSUER") - provider, err := rp.NewResourceServerFromKeyFile(keyPath) + provider, err := rs.NewResourceServerFromKeyFile(issuer, keyPath) if err != nil { logrus.Fatalf("error creating provider %s", err.Error()) } @@ -46,7 +47,7 @@ func main() { if !ok { return } - resp, err := rp.Introspect(r.Context(), provider, token) + resp, err := rs.Introspect(r.Context(), provider, token) if err != nil { http.Error(w, err.Error(), http.StatusForbidden) return @@ -67,7 +68,7 @@ func main() { if !ok { return } - resp, err := rp.Introspect(r.Context(), provider, token) + resp, err := rs.Introspect(r.Context(), provider, token) if err != nil { http.Error(w, err.Error(), http.StatusForbidden) return diff --git a/example/client/app/app.go b/example/client/app/app.go index c3bf9e0..e3ddd15 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -1,11 +1,8 @@ package main import ( - "context" "encoding/json" "fmt" - "html/template" - "io/ioutil" "net/http" "os" "strings" @@ -14,8 +11,8 @@ import ( "github.com/google/uuid" "github.com/sirupsen/logrus" + "github.com/caos/oidc/pkg/client/rp" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/rp" "github.com/caos/oidc/pkg/utils" ) @@ -32,8 +29,6 @@ func main() { port := os.Getenv("PORT") scopes := strings.Split(os.Getenv("SCOPES"), " ") - ctx := context.Background() - redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath) cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure()) @@ -48,7 +43,7 @@ func main() { options = append(options, rp.WithClientKey(keyPath)) } - provider, err := rp.NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes, options...) + provider, err := rp.NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes, options...) if err != nil { logrus.Fatalf("error creating provider %s", err.Error()) } @@ -81,80 +76,6 @@ func main() { //with the returned tokens from the token endpoint http.Handle(callbackPath, rp.CodeExchangeHandler(marshal, provider)) - http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { - tokens, err := rp.ClientCredentials(ctx, provider, "scope") - if err != nil { - http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized) - return - } - - data, err := json.Marshal(tokens) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.Write(data) - }) - - http.HandleFunc("/jwt-profile", func(w http.ResponseWriter, r *http.Request) { - if r.Method == "GET" { - tpl := ` - - - - - Login - - -
- - - -
- - ` - t, err := template.New("login").Parse(tpl) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - err = t.Execute(w, nil) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - } else { - err := r.ParseMultipartForm(4 << 10) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - file, handler, err := r.FormFile("key") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - defer file.Close() - - key, err := ioutil.ReadAll(file) - fmt.Println(handler.Header) - assertion, err := oidc.NewJWTProfileAssertionFromFileData(key, []string{issuer}) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - token, err := rp.JWTProfileAssertionExchange(ctx, assertion, scopes, provider) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - data, err := json.Marshal(token) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.Write(data) - } - }) lis := fmt.Sprintf("127.0.0.1:%s", port) logrus.Infof("listening on http://%s/", lis) logrus.Fatal(http.ListenAndServe("127.0.0.1:"+port, nil)) diff --git a/example/client/github/github.go b/example/client/github/github.go index c136091..f39c40b 100644 --- a/example/client/github/github.go +++ b/example/client/github/github.go @@ -10,8 +10,8 @@ import ( "golang.org/x/oauth2" githubOAuth "golang.org/x/oauth2/github" - "github.com/caos/oidc/pkg/rp" - "github.com/caos/oidc/pkg/rp/cli" + "github.com/caos/oidc/pkg/client/rp" + "github.com/caos/oidc/pkg/client/rp/cli" "github.com/caos/oidc/pkg/utils" ) @@ -35,7 +35,7 @@ func main() { ctx := context.Background() cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure()) - relayingParty, err := rp.NewRelayingPartyOAuth(rpConfig, rp.WithCookieHandler(cookieHandler)) + relyingParty, err := rp.NewRelyingPartyOAuth(rpConfig, rp.WithCookieHandler(cookieHandler)) if err != nil { fmt.Printf("error creating relaying party: %v", err) return @@ -43,9 +43,9 @@ func main() { state := func() string { return uuid.New().String() } - token := cli.CodeFlow(relayingParty, callbackPath, port, state) + token := cli.CodeFlow(relyingParty, callbackPath, port, state) - client := github.NewClient(relayingParty.OAuthConfig().Client(ctx, token.Token)) + client := github.NewClient(relyingParty.OAuthConfig().Client(ctx, token.Token)) _, _, err = client.Users.Get(ctx, "") if err != nil { diff --git a/example/client/service/service.go b/example/client/service/service.go new file mode 100644 index 0000000..95227d0 --- /dev/null +++ b/example/client/service/service.go @@ -0,0 +1,196 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "html/template" + "io/ioutil" + "net/http" + "os" + "strings" + + "github.com/sirupsen/logrus" + "golang.org/x/oauth2" + + "github.com/caos/oidc/pkg/client/profile" +) + +var ( + client *http.Client = http.DefaultClient +) + +func main() { + //keyPath := os.Getenv("KEY_PATH") + issuer := os.Getenv("ISSUER") + port := os.Getenv("PORT") + scopes := strings.Split(os.Getenv("SCOPES"), " ") + //testURL := os.Getenv("TEST_URL") + + //if keyPath != "" { + // ts, err := rp.NewJWTProfileTokenSourceFromFile(issuer, keyPath, scopes) + // if err != nil { + // logrus.Fatalf("error creating token source %s", err.Error()) + // } + // //client = oauth2.NewClient(context.Background(), ts) + // resp, err := callExampleEndpoint(client, testURL) + // if err != nil { + // logrus.Fatalf("error response from test url: %s", err.Error()) + // } + // fmt.Println(resp) + //} + + http.HandleFunc("/jwt-profile", func(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + tpl := ` + + + + + Login + + +
+ + + +
+ + ` + t, err := template.New("login").Parse(tpl) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + err = t.Execute(w, nil) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } else { + err := r.ParseMultipartForm(4 << 10) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + file, _, err := r.FormFile("key") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer file.Close() + + key, err := ioutil.ReadAll(file) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + ts, err := profile.NewJWTProfileTokenSourceFromKeyFile(issuer, key, scopes) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + client = oauth2.NewClient(context.Background(), ts) + token, err := ts.Token() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + //assertion, err := oidc.NewJWTProfileAssertionFromFileData(key, []string{issuer}) + //if err != nil { + // http.Error(w, err.Error(), http.StatusInternalServerError) + // return + //} + //token, err := rp.JWTProfileAssertionExchange(ctx, assertion, scopes, provider) + //if err != nil { + // http.Error(w, err.Error(), http.StatusInternalServerError) + // return + //} + data, err := json.Marshal(token) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Write(data) + } + }) + + http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { + tpl := ` + + + + + Test + + +
+ + + +
+ {{if .URL}} +

+ Result for {{.URL}}: {{.Response}} +

+ {{end}} + + ` + err := r.ParseForm() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + testURL := r.Form.Get("url") + var data struct { + URL string + Response interface{} + } + if testURL != "" { + data.URL = testURL + data.Response, err = callExampleEndpoint(client, testURL) + if err != nil { + data.Response = err + } + } + t, err := template.New("login").Parse(tpl) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + err = t.Execute(w, data) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + }) + lis := fmt.Sprintf("127.0.0.1:%s", port) + logrus.Infof("listening on http://%s/", lis) + logrus.Fatal(http.ListenAndServe("127.0.0.1:"+port, nil)) +} + +func callExampleEndpoint(client *http.Client, testURL string) (interface{}, error) { + req, err := http.NewRequest("GET", testURL, nil) + if err != nil { + return nil, err + } + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("http status not ok: %s %s", resp.Status, body) + } + + if strings.HasPrefix(resp.Header.Get("content-type"), "text/plain") { + return string(body), nil + } + return body, err +} diff --git a/pkg/client/client.go b/pkg/client/client.go new file mode 100644 index 0000000..b2b815e --- /dev/null +++ b/pkg/client/client.go @@ -0,0 +1,90 @@ +package client + +import ( + "net/http" + "reflect" + "strings" + "time" + + "github.com/gorilla/schema" + "golang.org/x/oauth2" + "gopkg.in/square/go-jose.v2" + + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/utils" +) + +var ( + Encoder = func() utils.Encoder { + e := schema.NewEncoder() + e.RegisterEncoder(oidc.Scopes{}, func(value reflect.Value) string { + return value.Interface().(oidc.Scopes).Encode() + }) + return e + }() +) + +//Discover calls the discovery endpoint of the provided issuer and returns its configuration +func Discover(issuer string, httpClient *http.Client) (*oidc.DiscoveryConfiguration, error) { + wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint + req, err := http.NewRequest("GET", wellKnown, nil) + if err != nil { + return nil, err + } + discoveryConfig := new(oidc.DiscoveryConfiguration) + err = utils.HttpRequest(httpClient, req, &discoveryConfig) + if err != nil { + return nil, err + } + return discoveryConfig, nil +} + +type tokenEndpointCaller interface { + TokenEndpoint() string + HttpClient() *http.Client +} + +func CallTokenEndpoint(request interface{}, caller tokenEndpointCaller) (newToken *oauth2.Token, err error) { + return callTokenEndpoint(request, nil, caller) +} + +func callTokenEndpoint(request interface{}, authFn interface{}, caller tokenEndpointCaller) (newToken *oauth2.Token, err error) { + req, err := utils.FormRequest(caller.TokenEndpoint(), request, Encoder, authFn) + if err != nil { + return nil, err + } + tokenRes := new(oidc.AccessTokenResponse) + if err := utils.HttpRequest(caller.HttpClient(), req, &tokenRes); err != nil { + return nil, err + } + return &oauth2.Token{ + AccessToken: tokenRes.AccessToken, + TokenType: tokenRes.TokenType, + RefreshToken: tokenRes.RefreshToken, + Expiry: time.Now().UTC().Add(time.Duration(tokenRes.ExpiresIn) * time.Second), + }, nil +} + +func NewSignerFromPrivateKeyByte(key []byte, keyID string) (jose.Signer, error) { + privateKey, err := utils.BytesToPrivateKey(key) + if err != nil { + return nil, err + } + signingKey := jose.SigningKey{ + Algorithm: jose.RS256, + Key: &jose.JSONWebKey{Key: privateKey, KeyID: keyID}, + } + return jose.NewSigner(signingKey, &jose.SignerOptions{}) +} + +func SignedJWTProfileAssertion(clientID string, audience []string, expiration time.Duration, signer jose.Signer) (string, error) { + iat := time.Now() + exp := iat.Add(expiration) + return utils.Sign(&oidc.JWTTokenRequest{ + Issuer: clientID, + Subject: clientID, + Audience: audience, + ExpiresAt: oidc.Time(exp), + IssuedAt: oidc.Time(iat), + }, signer) +} diff --git a/pkg/client/jwt_profile.go b/pkg/client/jwt_profile.go new file mode 100644 index 0000000..8095588 --- /dev/null +++ b/pkg/client/jwt_profile.go @@ -0,0 +1,30 @@ +package client + +import ( + "context" + "net/url" + + "golang.org/x/oauth2" + + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/utils" +) + +//JWTProfileExchange handles the oauth2 jwt profile exchange +func JWTProfileExchange(ctx context.Context, jwtProfileGrantRequest *oidc.JWTProfileGrantRequest, caller tokenEndpointCaller) (*oauth2.Token, error) { + return CallTokenEndpoint(jwtProfileGrantRequest, caller) +} + +func ClientAssertionCodeOptions(assertion string) []oauth2.AuthCodeOption { + return []oauth2.AuthCodeOption{ + oauth2.SetAuthURLParam("client_assertion", assertion), + oauth2.SetAuthURLParam("client_assertion_type", oidc.ClientAssertionTypeJWTAssertion), + } +} + +func ClientAssertionFormAuthorization(assertion string) utils.FormAuthorization { + return func(values url.Values) { + values.Set("client_assertion", assertion) + values.Set("client_assertion_type", oidc.ClientAssertionTypeJWTAssertion) + } +} diff --git a/pkg/rp/key.go b/pkg/client/key.go similarity index 78% rename from pkg/rp/key.go rename to pkg/client/key.go index 26d8bf5..f89a2b4 100644 --- a/pkg/rp/key.go +++ b/pkg/client/key.go @@ -1,4 +1,4 @@ -package rp +package client import ( "encoding/json" @@ -14,7 +14,7 @@ type keyFile struct { Type string `json:"type"` // serviceaccount or application KeyID string `json:"keyId"` Key string `json:"key"` - Issuer string `json:"issuer"` + Issuer string `json:"issuer"` //not yet in file //serviceaccount UserID string `json:"userId"` @@ -28,6 +28,10 @@ func ConfigFromKeyFile(path string) (*keyFile, error) { if err != nil { return nil, err } + return ConfigFromKeyFileData(data) +} + +func ConfigFromKeyFileData(data []byte) (*keyFile, error) { var f keyFile if err := json.Unmarshal(data, &f); err != nil { return nil, err diff --git a/pkg/client/profile/jwt_profile.go b/pkg/client/profile/jwt_profile.go new file mode 100644 index 0000000..d60a2f8 --- /dev/null +++ b/pkg/client/profile/jwt_profile.go @@ -0,0 +1,85 @@ +package profile + +import ( + "net/http" + "time" + + "golang.org/x/oauth2" + "gopkg.in/square/go-jose.v2" + + "github.com/caos/oidc/pkg/client" + "github.com/caos/oidc/pkg/oidc" +) + +//jwtProfileTokenSource implement the oauth2.TokenSource +//it will request a token using the OAuth2 JWT Profile Grant +//therefore sending an `assertion` by singing a JWT with the provided private key +type jwtProfileTokenSource struct { + clientID string + audience []string + signer jose.Signer + scopes []string + httpClient *http.Client + tokenEndpoint string +} + +func NewJWTProfileTokenSourceFromKeyFile(issuer string, data []byte, scopes []string, options ...func(source *jwtProfileTokenSource)) (oauth2.TokenSource, error) { + keyData, err := client.ConfigFromKeyFileData(data) + if err != nil { + return nil, err + } + return NewJWTProfileTokenSource(issuer, keyData.UserID, keyData.KeyID, []byte(keyData.Key), scopes, options...) +} + +func NewJWTProfileTokenSource(issuer, clientID, keyID string, key []byte, scopes []string, options ...func(source *jwtProfileTokenSource)) (oauth2.TokenSource, error) { + signer, err := client.NewSignerFromPrivateKeyByte(key, keyID) + if err != nil { + return nil, err + } + source := &jwtProfileTokenSource{ + clientID: clientID, + audience: []string{issuer}, + signer: signer, + scopes: scopes, + httpClient: http.DefaultClient, + } + for _, opt := range options { + opt(source) + } + if source.tokenEndpoint == "" { + config, err := client.Discover(issuer, source.httpClient) + if err != nil { + return nil, err + } + source.tokenEndpoint = config.TokenEndpoint + } + return source, nil +} + +func WithHTTPClient(client *http.Client) func(*jwtProfileTokenSource) { + return func(source *jwtProfileTokenSource) { + source.httpClient = client + } +} + +func WithStaticTokenEndpoint(issuer, tokenEndpoint string) func(*jwtProfileTokenSource) { + return func(source *jwtProfileTokenSource) { + source.tokenEndpoint = tokenEndpoint + } +} + +func (j *jwtProfileTokenSource) TokenEndpoint() string { + return j.tokenEndpoint +} + +func (j *jwtProfileTokenSource) HttpClient() *http.Client { + return j.httpClient +} + +func (j *jwtProfileTokenSource) Token() (*oauth2.Token, error) { + assertion, err := client.SignedJWTProfileAssertion(j.clientID, j.audience, time.Hour, j.signer) + if err != nil { + return nil, err + } + return client.JWTProfileExchange(nil, oidc.NewJWTProfileGrantRequest(assertion, j.scopes...), j) +} diff --git a/pkg/rp/cli/cli.go b/pkg/client/rp/cli/cli.go similarity index 67% rename from pkg/rp/cli/cli.go rename to pkg/client/rp/cli/cli.go index 4b00ba0..6cbb364 100644 --- a/pkg/rp/cli/cli.go +++ b/pkg/client/rp/cli/cli.go @@ -4,8 +4,8 @@ import ( "context" "net/http" + "github.com/caos/oidc/pkg/client/rp" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/rp" "github.com/caos/oidc/pkg/utils" ) @@ -13,7 +13,7 @@ const ( loginPath = "/login" ) -func CodeFlow(relayingParty rp.RelayingParty, callbackPath, port string, stateProvider func() string) *oidc.Tokens { +func CodeFlow(relyingParty rp.RelyingParty, callbackPath, port string, stateProvider func() string) *oidc.Tokens { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -24,8 +24,8 @@ func CodeFlow(relayingParty rp.RelayingParty, callbackPath, port string, statePr msg = msg + "

You are authenticated and can now return to the CLI.

" w.Write([]byte(msg)) } - http.Handle(loginPath, rp.AuthURLHandler(stateProvider, relayingParty)) - http.Handle(callbackPath, rp.CodeExchangeHandler(callback, relayingParty)) + http.Handle(loginPath, rp.AuthURLHandler(stateProvider, relyingParty)) + http.Handle(callbackPath, rp.CodeExchangeHandler(callback, relyingParty)) utils.StartServer(ctx, port) diff --git a/pkg/rp/delegation.go b/pkg/client/rp/delegation.go similarity index 100% rename from pkg/rp/delegation.go rename to pkg/client/rp/delegation.go diff --git a/pkg/rp/jwks.go b/pkg/client/rp/jwks.go similarity index 100% rename from pkg/rp/jwks.go rename to pkg/client/rp/jwks.go diff --git a/pkg/rp/mock/generate.go b/pkg/client/rp/mock/generate.go similarity index 100% rename from pkg/rp/mock/generate.go rename to pkg/client/rp/mock/generate.go diff --git a/pkg/rp/mock/verifier.mock.go b/pkg/client/rp/mock/verifier.mock.go similarity index 94% rename from pkg/rp/mock/verifier.mock.go rename to pkg/client/rp/mock/verifier.mock.go index acd7d77..08cf77f 100644 --- a/pkg/rp/mock/verifier.mock.go +++ b/pkg/client/rp/mock/verifier.mock.go @@ -5,10 +5,12 @@ package mock import ( - context "context" - oidc "github.com/caos/oidc/pkg/oidc" - gomock "github.com/golang/mock/gomock" - reflect "reflect" + "context" + "reflect" + + "github.com/golang/mock/gomock" + + "github.com/caos/oidc/pkg/oidc" ) // MockVerifier is a mock of Verifier interface diff --git a/pkg/rp/relaying_party.go b/pkg/client/rp/relaying_party.go similarity index 69% rename from pkg/rp/relaying_party.go rename to pkg/client/rp/relaying_party.go index 3260657..528f554 100644 --- a/pkg/rp/relaying_party.go +++ b/pkg/client/rp/relaying_party.go @@ -4,43 +4,32 @@ import ( "context" "errors" "net/http" - "net/url" - "reflect" "strings" "time" "github.com/google/uuid" - "github.com/gorilla/schema" - - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/oidc/grants" - "github.com/caos/oidc/pkg/utils" - "golang.org/x/oauth2" + "gopkg.in/square/go-jose.v2" + + "github.com/caos/oidc/pkg/client" + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/utils" ) const ( - idTokenKey = "id_token" - stateParam = "state" - pkceCode = "pkce" - jwtProfileKey = "urn:ietf:params:oauth:grant-type:jwt-bearer" + idTokenKey = "id_token" + stateParam = "state" + pkceCode = "pkce" ) -var ( - encoder = func() utils.Encoder { - e := schema.NewEncoder() - e.RegisterEncoder(oidc.Scopes{}, func(value reflect.Value) string { - return value.Interface().(oidc.Scopes).Encode() - }) - return e - }() -) - -//RelayingParty declares the minimal interface for oidc clients -type RelayingParty interface { +//RelyingParty declares the minimal interface for oidc clients +type RelyingParty interface { //OAuthConfig returns the oauth2 Config OAuthConfig() *oauth2.Config + //Issuer returns the issuer of the oidc config + Issuer() string + //IsPKCE returns if authorization is done using `Authorization Code Flow with Proof Key for Code Exchange (PKCE)` IsPKCE() bool @@ -53,13 +42,16 @@ type RelayingParty interface { //IsOAuth2Only specifies whether relaying party handles only oauth2 or oidc calls IsOAuth2Only() bool - ClientKey() []byte - ClientKeyID() string + //Signer is used if the relaying party uses the JWT Profile + Signer() jose.Signer + + //UserinfoEndpoint returns the userinfo + UserinfoEndpoint() string //IDTokenVerifier returns the verifier interface used for oidc id_token verification IDTokenVerifier() IDTokenVerifier - //ErrorHandler returns the handler used for callback errors + ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) } @@ -71,14 +63,12 @@ var ( } ) -type relayingParty struct { +type relyingParty struct { issuer string endpoints Endpoints oauthConfig *oauth2.Config oauth2Only bool pkce bool - clientKey []byte - clientKeyID string httpClient *http.Client cookieHandler *utils.CookieHandler @@ -86,72 +76,79 @@ type relayingParty struct { errorHandler func(http.ResponseWriter, *http.Request, string, string, string) idTokenVerifier IDTokenVerifier verifierOpts []VerifierOption + signer jose.Signer } -func (rp *relayingParty) OAuthConfig() *oauth2.Config { +func (rp *relyingParty) OAuthConfig() *oauth2.Config { return rp.oauthConfig } -func (rp *relayingParty) IsPKCE() bool { +func (rp *relyingParty) Issuer() string { + return rp.issuer +} + +func (rp *relyingParty) IsPKCE() bool { return rp.pkce } -func (rp *relayingParty) CookieHandler() *utils.CookieHandler { +func (rp *relyingParty) CookieHandler() *utils.CookieHandler { return rp.cookieHandler } -func (rp *relayingParty) HttpClient() *http.Client { +func (rp *relyingParty) HttpClient() *http.Client { return rp.httpClient } -func (rp *relayingParty) IsOAuth2Only() bool { +func (rp *relyingParty) IsOAuth2Only() bool { return rp.oauth2Only } -func (rp *relayingParty) ClientKey() []byte { - return rp.clientKey +func (rp *relyingParty) Signer() jose.Signer { + return rp.signer } -func (rp *relayingParty) ClientKeyID() string { - return rp.clientKeyID +func (rp *relyingParty) UserinfoEndpoint() string { + return rp.endpoints.UserinfoURL } -func (rp *relayingParty) IDTokenVerifier() IDTokenVerifier { +func (rp *relyingParty) IDTokenVerifier() IDTokenVerifier { if rp.idTokenVerifier == nil { rp.idTokenVerifier = NewIDTokenVerifier(rp.issuer, rp.oauthConfig.ClientID, NewRemoteKeySet(rp.httpClient, rp.endpoints.JKWsURL), rp.verifierOpts...) } return rp.idTokenVerifier } -func (rp *relayingParty) ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) { +func (rp *relyingParty) ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) { if rp.errorHandler == nil { rp.errorHandler = DefaultErrorHandler } return rp.errorHandler } -//NewRelayingPartyOAuth creates an (OAuth2) RelayingParty with the given +//NewRelyingPartyOAuth creates an (OAuth2) RelyingParty with the given //OAuth2 Config and possible configOptions //it will use the AuthURL and TokenURL set in config -func NewRelayingPartyOAuth(config *oauth2.Config, options ...Option) (RelayingParty, error) { - rp := &relayingParty{ +func NewRelyingPartyOAuth(config *oauth2.Config, options ...Option) (RelyingParty, error) { + rp := &relyingParty{ oauthConfig: config, httpClient: utils.DefaultHTTPClient, oauth2Only: true, } for _, optFunc := range options { - optFunc(rp) + if err := optFunc(rp); err != nil { + return nil, err + } } return rp, nil } -//NewRelayingPartyOIDC creates an (OIDC) RelayingParty with the given +//NewRelyingPartyOIDC creates an (OIDC) RelyingParty with the given //issuer, clientID, clientSecret, redirectURI, scopes and possible configOptions //it will run discovery on the provided issuer and use the found endpoints -func NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, scopes []string, options ...Option) (RelayingParty, error) { - rp := &relayingParty{ +func NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, scopes []string, options ...Option) (RelyingParty, error) { + rp := &relyingParty{ issuer: issuer, oauthConfig: &oauth2.Config{ ClientID: clientID, @@ -164,7 +161,9 @@ func NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, sc } for _, optFunc := range options { - optFunc(rp) + if err := optFunc(rp); err != nil { + return nil, err + } } endpoints, err := Discover(rp.issuer, rp.httpClient) @@ -178,12 +177,13 @@ func NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, sc } //DefaultRPOpts is the type for providing dynamic options to the DefaultRP -type Option func(*relayingParty) +type Option func(*relyingParty) error //WithCookieHandler set a `CookieHandler` for securing the various redirects func WithCookieHandler(cookieHandler *utils.CookieHandler) Option { - return func(rp *relayingParty) { + return func(rp *relyingParty) error { rp.cookieHandler = cookieHandler + return nil } } @@ -191,40 +191,49 @@ func WithCookieHandler(cookieHandler *utils.CookieHandler) Option { //it also sets a `CookieHandler` for securing the various redirects //and exchanging the code challenge func WithPKCE(cookieHandler *utils.CookieHandler) Option { - return func(rp *relayingParty) { + return func(rp *relyingParty) error { rp.pkce = true rp.cookieHandler = cookieHandler + return nil } } //WithHTTPClient provides the ability to set an http client to be used for the relaying party and verifier func WithHTTPClient(client *http.Client) Option { - return func(rp *relayingParty) { + return func(rp *relyingParty) error { rp.httpClient = client + return nil } } func WithErrorHandler(errorHandler ErrorHandler) Option { - return func(rp *relayingParty) { + return func(rp *relyingParty) error { rp.errorHandler = errorHandler + return nil } } func WithVerifierOpts(opts ...VerifierOption) Option { - return func(rp *relayingParty) { + return func(rp *relyingParty) error { rp.verifierOpts = opts + return nil } } func WithClientKey(path string) Option { - return func(rp *relayingParty) { - config, _ := ConfigFromKeyFile(path) - rp.clientKey = []byte(config.Key) - rp.clientKeyID = config.KeyID + return func(rp *relyingParty) error { + config, err := client.ConfigFromKeyFile(path) + if err != nil { + return err + } + rp.signer, err = client.NewSignerFromPrivateKeyByte([]byte(config.Key), config.KeyID) + return err } } //Discover calls the discovery endpoint of the provided issuer and returns the found endpoints +// +//deprecated: use client.Discover func Discover(issuer string, httpClient *http.Client) (Endpoints, error) { wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint req, err := http.NewRequest("GET", wellKnown, nil) @@ -241,7 +250,7 @@ func Discover(issuer string, httpClient *http.Client) (Endpoints, error) { //AuthURL returns the auth request url //(wrapping the oauth2 `AuthCodeURL`) -func AuthURL(state string, rp RelayingParty, opts ...AuthURLOpt) string { +func AuthURL(state string, rp RelyingParty, opts ...AuthURLOpt) string { authOpts := make([]oauth2.AuthCodeOption, 0) for _, opt := range opts { authOpts = append(authOpts, opt()...) @@ -251,7 +260,7 @@ func AuthURL(state string, rp RelayingParty, opts ...AuthURLOpt) string { //AuthURLHandler extends the `AuthURL` method with a http redirect handler //including handling setting cookie for secure `state` transfer -func AuthURLHandler(stateFn func() string, rp RelayingParty) http.HandlerFunc { +func AuthURLHandler(stateFn func() string, rp RelyingParty) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { opts := make([]AuthURLOpt, 0) state := stateFn() @@ -272,7 +281,7 @@ func AuthURLHandler(stateFn func() string, rp RelayingParty) http.HandlerFunc { } //GenerateAndStoreCodeChallenge generates a PKCE code challenge and stores its verifier into a secure cookie -func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelayingParty) (string, error) { +func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelyingParty) (string, error) { codeVerifier := uuid.New().String() if err := rp.CookieHandler().SetCookie(w, pkceCode, codeVerifier); err != nil { return "", err @@ -282,7 +291,7 @@ func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelayingParty) (str //CodeExchange handles the oauth2 code exchange, extracting and validating the id_token //returning it parsed together with the oauth2 tokens (access, refresh) -func CodeExchange(ctx context.Context, code string, rp RelayingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens, err error) { +func CodeExchange(ctx context.Context, code string, rp RelyingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens, err error) { ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient()) codeOpts := make([]oauth2.AuthCodeOption, 0) for _, opt := range opts { @@ -314,7 +323,7 @@ func CodeExchange(ctx context.Context, code string, rp RelayingParty, opts ...Co //CodeExchangeHandler extends the `CodeExchange` method with a http handler //including cookie handling for secure `state` transfer //and optional PKCE code verifier checking -func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc.Tokens, string), rp RelayingParty) http.HandlerFunc { +func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc.Tokens, string), rp RelyingParty) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { state, err := tryReadStateCookie(w, r, rp) if err != nil { @@ -335,8 +344,8 @@ func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc } codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier)) } - if len(rp.ClientKey()) > 0 { - assertion, err := oidc.GenerateJWTProfileToken(oidc.NewJWTProfileAssertion(rp.OAuthConfig().ClientID, rp.ClientKeyID(), []string{"http://localhost:50002/oauth/v2"}, rp.ClientKey())) + if rp.Signer() != nil { + assertion, err := client.SignedJWTProfileAssertion(rp.OAuthConfig().ClientID, []string{rp.Issuer()}, time.Hour, rp.Signer()) if err != nil { http.Error(w, "failed to build assertion: "+err.Error(), http.StatusUnauthorized) return @@ -352,51 +361,21 @@ func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc } } -//ClientCredentials is the `RelayingParty` interface implementation -//handling the oauth2 client credentials grant -func ClientCredentials(ctx context.Context, rp RelayingParty, scopes ...string) (newToken *oauth2.Token, err error) { - return CallTokenEndpointAuthorized(grants.ClientCredentialsGrantBasic(scopes...), rp) -} - -func CallTokenEndpointAuthorized(request interface{}, rp RelayingParty) (newToken *oauth2.Token, err error) { - config := rp.OAuthConfig() - var fn interface{} = utils.AuthorizeBasic(config.ClientID, config.ClientSecret) - if config.Endpoint.AuthStyle == oauth2.AuthStyleInParams { - fn = func(form url.Values) { - form.Set("client_id", config.ClientID) - form.Set("client_secret", config.ClientSecret) - } - } - return callTokenEndpoint(request, fn, rp) -} - -func CallTokenEndpoint(request interface{}, rp RelayingParty) (newToken *oauth2.Token, err error) { - return callTokenEndpoint(request, nil, rp) -} - -func callTokenEndpoint(request interface{}, authFn interface{}, rp RelayingParty) (newToken *oauth2.Token, err error) { - req, err := utils.FormRequest(rp.OAuthConfig().Endpoint.TokenURL, request, encoder, authFn) +//Userinfo will call the OIDC Userinfo Endpoint with the provided token +func Userinfo(token string, rp RelyingParty) (oidc.UserInfo, error) { + req, err := http.NewRequest("GET", rp.UserinfoEndpoint(), nil) if err != nil { return nil, err } - var tokenRes struct { - AccessToken string `json:"access_token"` - TokenType string `json:"token_type"` - ExpiresIn int64 `json:"expires_in"` - RefreshToken string `json:"refresh_token"` - } - if err := utils.HttpRequest(rp.HttpClient(), req, &tokenRes); err != nil { + req.Header.Set("authorization", token) + userinfo := oidc.NewUserInfo() + if err := utils.HttpRequest(rp.HttpClient(), req, &userinfo); err != nil { return nil, err } - return &oauth2.Token{ - AccessToken: tokenRes.AccessToken, - TokenType: tokenRes.TokenType, - RefreshToken: tokenRes.RefreshToken, - Expiry: time.Now().UTC().Add(time.Duration(tokenRes.ExpiresIn) * time.Second), - }, nil + return userinfo, nil } -func trySetStateCookie(w http.ResponseWriter, state string, rp RelayingParty) error { +func trySetStateCookie(w http.ResponseWriter, state string, rp RelyingParty) error { if rp.CookieHandler() != nil { if err := rp.CookieHandler().SetCookie(w, stateParam, state); err != nil { return err @@ -405,7 +384,7 @@ func trySetStateCookie(w http.ResponseWriter, state string, rp RelayingParty) er return nil } -func tryReadStateCookie(w http.ResponseWriter, r *http.Request, rp RelayingParty) (state string, err error) { +func tryReadStateCookie(w http.ResponseWriter, r *http.Request, rp RelyingParty) (state string, err error) { if rp.CookieHandler() == nil { return r.FormValue(stateParam), nil } @@ -417,7 +396,7 @@ func tryReadStateCookie(w http.ResponseWriter, r *http.Request, rp RelayingParty return state, nil } -type OptionFunc func(RelayingParty) +type OptionFunc func(RelyingParty) type Endpoints struct { oauth2.Endpoint @@ -472,9 +451,6 @@ func WithCodeVerifier(codeVerifier string) CodeExchangeOpt { //WithClientAssertionJWT sets the `client_assertion` param in the token request func WithClientAssertionJWT(clientAssertion string) CodeExchangeOpt { return func() []oauth2.AuthCodeOption { - return []oauth2.AuthCodeOption{ - oauth2.SetAuthURLParam("client_assertion", clientAssertion), - oauth2.SetAuthURLParam("client_assertion_type", oidc.ClientAssertionTypeJWTAssertion), - } + return client.ClientAssertionCodeOptions(clientAssertion) } } diff --git a/pkg/client/rp/tockenexchange.go b/pkg/client/rp/tockenexchange.go new file mode 100644 index 0000000..d5056ae --- /dev/null +++ b/pkg/client/rp/tockenexchange.go @@ -0,0 +1,27 @@ +package rp + +import ( + "context" + + "golang.org/x/oauth2" + + "github.com/caos/oidc/pkg/oidc/grants/tokenexchange" +) + +//TokenExchangeRP extends the `RelyingParty` interface for the *draft* oauth2 `Token Exchange` +type TokenExchangeRP interface { + RelyingParty + + //TokenExchange implement the `Token Exchange Grant` exchanging some token for an other + TokenExchange(context.Context, *tokenexchange.TokenExchangeRequest) (*oauth2.Token, error) +} + +//DelegationTokenExchangeRP extends the `TokenExchangeRP` interface +//for the specific `delegation token` request +type DelegationTokenExchangeRP interface { + TokenExchangeRP + + //DelegationTokenExchange implement the `Token Exchange Grant` + //providing an access token in request for a `delegation` token for a given resource / audience + DelegationTokenExchange(context.Context, string, ...tokenexchange.TokenExchangeOption) (*oauth2.Token, error) +} diff --git a/pkg/rp/verifier.go b/pkg/client/rp/verifier.go similarity index 92% rename from pkg/rp/verifier.go rename to pkg/client/rp/verifier.go index a156f6d..1f45ca8 100644 --- a/pkg/rp/verifier.go +++ b/pkg/client/rp/verifier.go @@ -214,13 +214,3 @@ func (i *idTokenVerifier) ACR() oidc.ACRVerifier { func (i *idTokenVerifier) MaxAge() time.Duration { return i.maxAge } - -//deprecated: Use IDTokenVerifier (or oidc.Verifier) -type Verifier interface { - - //Verify checks the access_token and id_token and returns the `id token claims` - Verify(ctx context.Context, accessToken, idTokenString string) (*oidc.IDTokenClaims, error) - - //VerifyIDToken checks the id_token only and returns its `id token claims` - VerifyIDToken(ctx context.Context, idTokenString string) (*oidc.IDTokenClaims, error) -} diff --git a/pkg/client/rs/resource_server.go b/pkg/client/rs/resource_server.go new file mode 100644 index 0000000..f5dbe69 --- /dev/null +++ b/pkg/client/rs/resource_server.go @@ -0,0 +1,123 @@ +package rs + +import ( + "context" + "errors" + "net/http" + "time" + + "github.com/caos/oidc/pkg/client" + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/utils" +) + +type ResourceServer interface { + IntrospectionURL() string + HttpClient() *http.Client + AuthFn() (interface{}, error) +} + +type resourceServer struct { + issuer string + tokenURL string + introspectURL string + httpClient *http.Client + authFn func() (interface{}, error) +} + +func (r *resourceServer) IntrospectionURL() string { + return r.introspectURL +} + +func (r *resourceServer) HttpClient() *http.Client { + return r.httpClient +} + +func (r *resourceServer) AuthFn() (interface{}, error) { + return r.authFn() +} + +func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, option Option) (ResourceServer, error) { + authorizer := func() (interface{}, error) { + return utils.AuthorizeBasic(clientID, clientSecret), nil + } + return newResourceServer(issuer, authorizer, option) +} +func NewResourceServerJWTProfile(issuer, clientID, keyID string, key []byte, options ...Option) (ResourceServer, error) { + signer, err := client.NewSignerFromPrivateKeyByte(key, keyID) + if err != nil { + return nil, err + } + authorizer := func() (interface{}, error) { + assertion, err := client.SignedJWTProfileAssertion(clientID, []string{issuer}, time.Hour, signer) + if err != nil { + return nil, err + } + return client.ClientAssertionFormAuthorization(assertion), nil + } + return newResourceServer(issuer, authorizer, options...) +} + +func newResourceServer(issuer string, authorizer func() (interface{}, error), options ...Option) (*resourceServer, error) { + rs := &resourceServer{ + issuer: issuer, + httpClient: utils.DefaultHTTPClient, + } + for _, optFunc := range options { + optFunc(rs) + } + if rs.introspectURL == "" || rs.tokenURL == "" { + config, err := client.Discover(rs.issuer, rs.httpClient) + if err != nil { + return nil, err + } + rs.tokenURL = config.TokenEndpoint + rs.introspectURL = config.IntrospectionEndpoint + } + if rs.introspectURL == "" || rs.tokenURL == "" { + return nil, errors.New("introspectURL and/or tokenURL is empty: please provide with either `WithStaticEndpoints` or a discovery url") + } + rs.authFn = authorizer + return rs, nil +} + +func NewResourceServerFromKeyFile(issuer, path string, options ...Option) (ResourceServer, error) { + c, err := client.ConfigFromKeyFile(path) + if err != nil { + return nil, err + } + return NewResourceServerJWTProfile(issuer, c.ClientID, c.KeyID, []byte(c.Key), options...) +} + +type Option func(*resourceServer) + +//WithClient provides the ability to set an http client to be used for the resource server +func WithClient(client *http.Client) Option { + return func(server *resourceServer) { + server.httpClient = client + } +} + +//WithStaticEndpoints provides the ability to set static token and introspect URL +func WithStaticEndpoints(tokenURL, introspectURL string) Option { + return func(server *resourceServer) { + server.tokenURL = tokenURL + server.introspectURL = introspectURL + } +} + +func Introspect(ctx context.Context, rp ResourceServer, token string) (oidc.IntrospectionResponse, error) { + authFn, err := rp.AuthFn() + if err != nil { + return nil, err + } + req, err := utils.FormRequest(rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, client.Encoder, authFn) + if err != nil { + return nil, err + } + resp := oidc.NewIntrospectionResponse() + if err := utils.HttpRequest(rp.HttpClient(), req, resp); err != nil { + return nil, err + } + return resp, nil +} diff --git a/pkg/oidc/discovery.go b/pkg/oidc/discovery.go index 104078c..ef1d65e 100644 --- a/pkg/oidc/discovery.go +++ b/pkg/oidc/discovery.go @@ -42,6 +42,7 @@ type DiscoveryConfiguration struct { DisplayValuesSupported []Display `json:"display_values_supported,omitempty"` ClaimTypesSupported []string `json:"claim_types_supported,omitempty"` ClaimsSupported []string `json:"claims_supported,omitempty"` + ClaimsParameterSupported bool `json:"claims_parameter_supported,omitempty"` CodeChallengeMethodsSupported []CodeChallengeMethod `json:"code_challenge_methods_supported,omitempty"` ServiceDocumentation string `json:"service_documentation,omitempty"` ClaimsLocalesSupported []language.Tag `json:"claims_locales_supported,omitempty"` diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 967a24c..33ed6ce 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -34,11 +34,6 @@ type OPStorage interface { GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]interface{}, error) GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) ValidateJWTProfileScopes(ctx context.Context, userID string, scope oidc.Scopes) (oidc.Scopes, error) - - //deprecated: use GetUserinfoFromScopes instead - GetUserinfoFromScopes(ctx context.Context, userID, clientID string, scopes []string) (oidc.UserInfo, error) - //deprecated: use SetUserinfoFromToken instead - GetUserinfoFromToken(ctx context.Context, tokenID, subject, origin string) (oidc.UserInfo, error) } type Storage interface { diff --git a/pkg/op/token.go b/pkg/op/token.go index 5331d44..334bec9 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -114,7 +114,8 @@ func CreateIDToken(ctx context.Context, issuer string, authReq AuthRequest, vali } } if len(scopes) > 0 { - userInfo, err := storage.GetUserinfoFromScopes(ctx, authReq.GetSubject(), authReq.GetClientID(), scopes) + userInfo := oidc.NewUserInfo() + err := storage.SetUserinfoFromScopes(ctx, userInfo, authReq.GetSubject(), authReq.GetClientID(), scopes) if err != nil { return "", err } diff --git a/pkg/op/tokenrequest.go b/pkg/op/tokenrequest.go index e0729bf..b51d2c8 100644 --- a/pkg/op/tokenrequest.go +++ b/pkg/op/tokenrequest.go @@ -123,6 +123,9 @@ func AuthorizeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exc if err != nil { return nil, nil, err } + if client.AuthMethod() == oidc.AuthMethodPrivateKeyJWT { + return nil, nil, errors.New("invalid_grant") + } if client.AuthMethod() == oidc.AuthMethodNone { authReq, err := AuthorizeCodeChallenge(ctx, tokenReq, exchanger) return authReq, client, err diff --git a/pkg/op/userinfo.go b/pkg/op/userinfo.go index d951136..9abf378 100644 --- a/pkg/op/userinfo.go +++ b/pkg/op/userinfo.go @@ -34,7 +34,8 @@ func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoP http.Error(w, "access token invalid", http.StatusUnauthorized) return } - info, err := userinfoProvider.Storage().GetUserinfoFromToken(r.Context(), tokenID, subject, r.Header.Get("origin")) + info := oidc.NewUserInfo() + err = userinfoProvider.Storage().SetUserinfoFromToken(r.Context(), info, tokenID, subject, r.Header.Get("origin")) if err != nil { w.WriteHeader(http.StatusForbidden) utils.MarshalJSON(w, err) diff --git a/pkg/rp/resource_server.go b/pkg/rp/resource_server.go deleted file mode 100644 index c59097d..0000000 --- a/pkg/rp/resource_server.go +++ /dev/null @@ -1,184 +0,0 @@ -package rp - -import ( - "context" - "errors" - "net/http" - "net/url" - "time" - - "golang.org/x/oauth2" - "golang.org/x/oauth2/clientcredentials" - - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/utils" -) - -type ResourceServer interface { - IntrospectionURL() string - HttpClient() *http.Client - AuthFn() interface{} -} - -type resourceServer struct { - issuer string - tokenURL string - introspectURL string - httpClient *http.Client - authFn interface{} -} - -type jwtAccessTokenSource struct { - clientID string - audience []string - PrivateKey []byte - PrivateKeyID string -} - -func (j *jwtAccessTokenSource) Token() (*oauth2.Token, error) { - iat := time.Now() - exp := iat.Add(time.Hour) - assertion, err := GenerateJWTProfileToken(&oidc.JWTProfileAssertion{ - PrivateKeyID: j.PrivateKeyID, - PrivateKey: j.PrivateKey, - Issuer: j.clientID, - Subject: j.clientID, - Audience: j.audience, - Expiration: oidc.Time(exp), - IssuedAt: oidc.Time(iat), - }) - if err != nil { - return nil, err - } - return &oauth2.Token{AccessToken: assertion, TokenType: "Bearer", Expiry: exp}, nil -} - -func (r *resourceServer) IntrospectionURL() string { - return r.introspectURL -} - -func (r *resourceServer) HttpClient() *http.Client { - return r.httpClient -} - -func (r *resourceServer) AuthFn() interface{} { - return r.authFn -} - -func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, option RSOption) (ResourceServer, error) { - authorizer := func(tokenURL string) func(context.Context) *http.Client { - return (&clientcredentials.Config{ - ClientID: clientID, - ClientSecret: clientSecret, - TokenURL: tokenURL, - }).Client - } - return newResourceServer(issuer, authorizer, option) -} -func NewResourceServerJWTProfile(issuer, clientID, keyID string, key []byte, options ...RSOption) (ResourceServer, error) { - ts := &jwtAccessTokenSource{ - clientID: clientID, - PrivateKey: key, - PrivateKeyID: keyID, - audience: []string{issuer}, - } - - //authorizer := func(tokenURL string) func(context.Context) *http.Client { - // return func(ctx context.Context) *http.Client { - // return oauth2.NewClient(ctx, oauth2.ReuseTokenSource(token, ts)) - // } - //} - authorizer := utils.FormAuthorization(func(values url.Values) { - token, err := ts.Token() - if err != nil { - //return nil, err - } - values.Set("client_assertion", token.AccessToken) - }) - return newResourceServer(issuer, authorizer, options...) -} - -// -//func newResourceServer(issuer string, authorizer func(tokenURL string) func(ctx context.Context) *http.Client, options ...RSOption) (*resourceServer, error) { -// rp := &resourceServer{ -// issuer: issuer, -// httpClient: utils.DefaultHTTPClient, -// } -// for _, optFunc := range options { -// optFunc(rp) -// } -// if rp.introspectURL == "" || rp.tokenURL == "" { -// endpoints, err := Discover(rp.issuer, rp.httpClient) -// if err != nil { -// return nil, err -// } -// rp.tokenURL = endpoints.TokenURL -// rp.introspectURL = endpoints.IntrospectURL -// } -// if rp.introspectURL == "" || rp.tokenURL == "" { -// return nil, errors.New("introspectURL and/or tokenURL is empty: please provide with either `WithStaticEndpoints` or a discovery url") -// } -// //rp.httpClient = authorizer(rp.tokenURL)(context.WithValue(context.Background(), oauth2.HTTPClient, rp.HttpClient())) -// return rp, nil -//} -func newResourceServer(issuer string, authorizer interface{}, options ...RSOption) (*resourceServer, error) { - rp := &resourceServer{ - issuer: issuer, - httpClient: utils.DefaultHTTPClient, - } - for _, optFunc := range options { - optFunc(rp) - } - if rp.introspectURL == "" || rp.tokenURL == "" { - endpoints, err := Discover(rp.issuer, rp.httpClient) - if err != nil { - return nil, err - } - rp.tokenURL = endpoints.TokenURL - rp.introspectURL = endpoints.IntrospectURL - } - if rp.introspectURL == "" || rp.tokenURL == "" { - return nil, errors.New("introspectURL and/or tokenURL is empty: please provide with either `WithStaticEndpoints` or a discovery url") - } - //rp.httpClient = authorizer(rp.tokenURL)(context.WithValue(context.Background(), oauth2.HTTPClient, rp.HttpClient())) - rp.authFn = authorizer - return rp, nil -} - -func NewResourceServerFromKeyFile(path string, options ...RSOption) (ResourceServer, error) { - c, err := ConfigFromKeyFile(path) - if err != nil { - return nil, err - } - c.Issuer = "http://localhost:50002/oauth/v2" - return NewResourceServerJWTProfile(c.Issuer, c.ClientID, c.KeyID, []byte(c.Key), options...) -} - -type RSOption func(*resourceServer) - -//WithClient provides the ability to set an http client to be used for the resource server -func WithClient(client *http.Client) RSOption { - return func(server *resourceServer) { - server.httpClient = client - } -} - -//WithStaticEndpoints provides the ability to set static token and introspect URL -func WithStaticEndpoints(tokenURL, introspectURL string) RSOption { - return func(server *resourceServer) { - server.tokenURL = tokenURL - server.introspectURL = introspectURL - } -} - -func Introspect(ctx context.Context, rp ResourceServer, token string) (oidc.IntrospectionResponse, error) { - req, err := utils.FormRequest(rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, encoder, rp.AuthFn()) - if err != nil { - return nil, err - } - resp := oidc.NewIntrospectionResponse() - if err := utils.HttpRequest(rp.HttpClient(), req, resp); err != nil { - return nil, err - } - return resp, nil -} diff --git a/pkg/rp/tockenexchange.go b/pkg/rp/tockenexchange.go deleted file mode 100644 index bdca44b..0000000 --- a/pkg/rp/tockenexchange.go +++ /dev/null @@ -1,100 +0,0 @@ -package rp - -import ( - "context" - "crypto/rsa" - "crypto/x509" - "encoding/json" - "encoding/pem" - - "golang.org/x/oauth2" - "gopkg.in/square/go-jose.v2" - - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/oidc/grants/tokenexchange" -) - -//TokenExchangeRP extends the `RelayingParty` interface for the *draft* oauth2 `Token Exchange` -type TokenExchangeRP interface { - RelayingParty - - //TokenExchange implement the `Token Exchange Grant` exchanging some token for an other - TokenExchange(context.Context, *tokenexchange.TokenExchangeRequest) (*oauth2.Token, error) -} - -//DelegationTokenExchangeRP extends the `TokenExchangeRP` interface -//for the specific `delegation token` request -type DelegationTokenExchangeRP interface { - TokenExchangeRP - - //DelegationTokenExchange implement the `Token Exchange Grant` - //providing an access token in request for a `delegation` token for a given resource / audience - DelegationTokenExchange(context.Context, string, ...tokenexchange.TokenExchangeOption) (*oauth2.Token, error) -} - -//TokenExchange handles the oauth2 token exchange -func TokenExchange(ctx context.Context, request *tokenexchange.TokenExchangeRequest, rp RelayingParty) (newToken *oauth2.Token, err error) { - return CallTokenEndpoint(request, rp) -} - -//DelegationTokenExchange handles the oauth2 token exchange for a delegation token -func DelegationTokenExchange(ctx context.Context, subjectToken string, rp RelayingParty, reqOpts ...tokenexchange.TokenExchangeOption) (newToken *oauth2.Token, err error) { - return TokenExchange(ctx, DelegationTokenRequest(subjectToken, reqOpts...), rp) -} - -//JWTProfileExchange handles the oauth2 jwt profile exchange -func JWTProfileExchange(ctx context.Context, jwtProfileGrantRequest *oidc.JWTProfileGrantRequest, rp RelayingParty) (*oauth2.Token, error) { - return CallTokenEndpoint(jwtProfileGrantRequest, rp) -} - -//JWTProfileExchange handles the oauth2 jwt profile exchange -func JWTProfileAssertionExchange(ctx context.Context, assertion *oidc.JWTProfileAssertion, scopes oidc.Scopes, rp RelayingParty) (*oauth2.Token, error) { - token, err := GenerateJWTProfileToken(assertion) - if err != nil { - return nil, err - } - return JWTProfileExchange(ctx, oidc.NewJWTProfileGrantRequest(token, scopes...), rp) -} - -func GenerateJWTProfileToken(assertion *oidc.JWTProfileAssertion) (string, error) { - privateKey, err := bytesToPrivateKey(assertion.PrivateKey) - if err != nil { - return "", err - } - key := jose.SigningKey{ - Algorithm: jose.RS256, - Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.PrivateKeyID}, - } - signer, err := jose.NewSigner(key, &jose.SignerOptions{}) - if err != nil { - return "", err - } - - marshalledAssertion, err := json.Marshal(assertion) - if err != nil { - return "", err - } - signedAssertion, err := signer.Sign(marshalledAssertion) - if err != nil { - return "", err - } - return signedAssertion.CompactSerialize() -} - -func bytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) { - block, _ := pem.Decode(priv) - enc := x509.IsEncryptedPEMBlock(block) - b := block.Bytes - var err error - if enc { - b, err = x509.DecryptPEMBlock(block, nil) - if err != nil { - return nil, err - } - } - key, err := x509.ParsePKCS1PrivateKey(b) - if err != nil { - return nil, err - } - return key, nil -} diff --git a/pkg/utils/key.go b/pkg/utils/key.go new file mode 100644 index 0000000..7965c85 --- /dev/null +++ b/pkg/utils/key.go @@ -0,0 +1,25 @@ +package utils + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" +) + +func BytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) { + block, _ := pem.Decode(priv) + enc := x509.IsEncryptedPEMBlock(block) + b := block.Bytes + var err error + if enc { + b, err = x509.DecryptPEMBlock(block, nil) + if err != nil { + return nil, err + } + } + key, err := x509.ParsePKCS1PrivateKey(b) + if err != nil { + return nil, err + } + return key, nil +} From 01ff740f4e6960053b8eec60c37fe876d3c77e77 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Fri, 12 Feb 2021 06:47:16 +0100 Subject: [PATCH 10/14] fixes --- pkg/oidc/discovery.go | 2 +- pkg/op/discovery_test.go | 31 +++++++++++++------ pkg/op/mock/storage.mock.go | 59 ++++++++++++++++++------------------- 3 files changed, 52 insertions(+), 40 deletions(-) diff --git a/pkg/oidc/discovery.go b/pkg/oidc/discovery.go index ef1d65e..acab578 100644 --- a/pkg/oidc/discovery.go +++ b/pkg/oidc/discovery.go @@ -19,7 +19,7 @@ type DiscoveryConfiguration struct { CheckSessionIframe string `json:"check_session_iframe,omitempty"` JwksURI string `json:"jwks_uri,omitempty"` ScopesSupported []string `json:"scopes_supported,omitempty"` - ResponseTypesSupported []string `json:"response_types_supported"` + ResponseTypesSupported []string `json:"response_types_supported,omitempty"` ResponseModesSupported []string `json:"response_modes_supported,omitempty"` GrantTypesSupported []GrantType `json:"grant_types_supported,omitempty"` ACRValuesSupported []string `json:"acr_values_supported,omitempty"` diff --git a/pkg/op/discovery_test.go b/pkg/op/discovery_test.go index e479faa..4d97a01 100644 --- a/pkg/op/discovery_test.go +++ b/pkg/op/discovery_test.go @@ -37,7 +37,7 @@ func TestDiscover(t *testing.T) { op.Discover(tt.args.w, tt.args.config) rec := tt.args.w.(*httptest.ResponseRecorder) require.Equal(t, http.StatusOK, rec.Code) - require.Equal(t, `{"issuer":"https://issuer.com"}`, rec.Body.String()) + require.Equal(t, `{"issuer":"https://issuer.com","request_uri_parameter_supported":false}`, rec.Body.String()) }) } } @@ -199,36 +199,49 @@ func Test_SubjectTypes(t *testing.T) { } } -func Test_AuthMethods(t *testing.T) { - m := mock.NewMockConfiguration(gomock.NewController(t)) +func Test_AuthMethodsTokenEndpoint(t *testing.T) { type args struct { c op.Configuration } tests := []struct { name string args args - want []string + want []oidc.AuthMethod }{ { - "imlicit basic", + "none and basic", args{func() op.Configuration { + m := mock.NewMockConfiguration(gomock.NewController(t)) m.EXPECT().AuthMethodPostSupported().Return(false) + m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false) return m }()}, - []string{string(oidc.AuthMethodBasic)}, + []oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic}, }, { - "basic and post", + "none, basic and post", args{func() op.Configuration { + m := mock.NewMockConfiguration(gomock.NewController(t)) m.EXPECT().AuthMethodPostSupported().Return(true) + m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false) return m }()}, - []string{string(oidc.AuthMethodBasic), string(oidc.AuthMethodPost)}, + []oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic, oidc.AuthMethodPost}, + }, + { + "none, basic, post and private_key_jwt", + args{func() op.Configuration { + m := mock.NewMockConfiguration(gomock.NewController(t)) + m.EXPECT().AuthMethodPostSupported().Return(true) + m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(true) + return m + }()}, + []oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic, oidc.AuthMethodPost, oidc.AuthMethodPrivateKeyJWT}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := op.AuthMethods(tt.args.c); !reflect.DeepEqual(got, tt.want) { + if got := op.AuthMethodsTokenEndpoint(tt.args.c); !reflect.DeepEqual(got, tt.want) { t.Errorf("authMethods() = %v, want %v", got, tt.want) } }) diff --git a/pkg/op/mock/storage.mock.go b/pkg/op/mock/storage.mock.go index b9adcec..e589413 100644 --- a/pkg/op/mock/storage.mock.go +++ b/pkg/op/mock/storage.mock.go @@ -198,36 +198,6 @@ func (mr *MockStorageMockRecorder) GetSigningKey(arg0, arg1, arg2, arg3 interfac return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSigningKey", reflect.TypeOf((*MockStorage)(nil).GetSigningKey), arg0, arg1, arg2, arg3) } -// GetUserinfoFromScopes mocks base method -func (m *MockStorage) GetUserinfoFromScopes(arg0 context.Context, arg1, arg2 string, arg3 []string) (oidc.UserInfo, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserinfoFromScopes", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(oidc.UserInfo) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUserinfoFromScopes indicates an expected call of GetUserinfoFromScopes -func (mr *MockStorageMockRecorder) GetUserinfoFromScopes(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserinfoFromScopes", reflect.TypeOf((*MockStorage)(nil).GetUserinfoFromScopes), arg0, arg1, arg2, arg3) -} - -// GetUserinfoFromToken mocks base method -func (m *MockStorage) GetUserinfoFromToken(arg0 context.Context, arg1, arg2, arg3 string) (oidc.UserInfo, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserinfoFromToken", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(oidc.UserInfo) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUserinfoFromToken indicates an expected call of GetUserinfoFromToken -func (mr *MockStorageMockRecorder) GetUserinfoFromToken(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserinfoFromToken", reflect.TypeOf((*MockStorage)(nil).GetUserinfoFromToken), arg0, arg1, arg2, arg3) -} - // Health mocks base method func (m *MockStorage) Health(arg0 context.Context) error { m.ctrl.T.Helper() @@ -270,6 +240,20 @@ func (mr *MockStorageMockRecorder) SaveNewKeyPair(arg0 interface{}) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveNewKeyPair", reflect.TypeOf((*MockStorage)(nil).SaveNewKeyPair), arg0) } +// SetIntrospectionFromToken mocks base method +func (m *MockStorage) SetIntrospectionFromToken(arg0 context.Context, arg1 oidc.IntrospectionResponse, arg2, arg3, arg4 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetIntrospectionFromToken", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetIntrospectionFromToken indicates an expected call of SetIntrospectionFromToken +func (mr *MockStorageMockRecorder) SetIntrospectionFromToken(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetIntrospectionFromToken", reflect.TypeOf((*MockStorage)(nil).SetIntrospectionFromToken), arg0, arg1, arg2, arg3, arg4) +} + // SetUserinfoFromScopes mocks base method func (m *MockStorage) SetUserinfoFromScopes(arg0 context.Context, arg1 oidc.UserInfoSetter, arg2, arg3 string, arg4 []string) error { m.ctrl.T.Helper() @@ -311,3 +295,18 @@ func (mr *MockStorageMockRecorder) TerminateSession(arg0, arg1, arg2 interface{} mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TerminateSession", reflect.TypeOf((*MockStorage)(nil).TerminateSession), arg0, arg1, arg2) } + +// ValidateJWTProfileScopes mocks base method +func (m *MockStorage) ValidateJWTProfileScopes(arg0 context.Context, arg1 string, arg2 oidc.Scopes) (oidc.Scopes, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidateJWTProfileScopes", arg0, arg1, arg2) + ret0, _ := ret[0].(oidc.Scopes) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ValidateJWTProfileScopes indicates an expected call of ValidateJWTProfileScopes +func (mr *MockStorageMockRecorder) ValidateJWTProfileScopes(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateJWTProfileScopes", reflect.TypeOf((*MockStorage)(nil).ValidateJWTProfileScopes), arg0, arg1, arg2) +} From 0c7b2605bdc5ccdb96a2305f15c396fb3f4a3a8a Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Fri, 12 Feb 2021 07:02:10 +0100 Subject: [PATCH 11/14] clenaup --- example/client/service/service.go | 34 ++++++++----------------------- example/internal/mock/storage.go | 21 ++++++++++++++----- pkg/client/profile/jwt_profile.go | 10 ++++++++- pkg/oidc/introspection.go | 10 +++------ pkg/oidc/types.go | 9 ++++---- pkg/oidc/userinfo.go | 5 ----- pkg/op/discovery.go | 24 +++++++++------------- pkg/utils/http.go | 3 --- 8 files changed, 51 insertions(+), 65 deletions(-) diff --git a/example/client/service/service.go b/example/client/service/service.go index 95227d0..34d959d 100644 --- a/example/client/service/service.go +++ b/example/client/service/service.go @@ -21,24 +21,18 @@ var ( ) func main() { - //keyPath := os.Getenv("KEY_PATH") + keyPath := os.Getenv("KEY_PATH") issuer := os.Getenv("ISSUER") port := os.Getenv("PORT") scopes := strings.Split(os.Getenv("SCOPES"), " ") - //testURL := os.Getenv("TEST_URL") - //if keyPath != "" { - // ts, err := rp.NewJWTProfileTokenSourceFromFile(issuer, keyPath, scopes) - // if err != nil { - // logrus.Fatalf("error creating token source %s", err.Error()) - // } - // //client = oauth2.NewClient(context.Background(), ts) - // resp, err := callExampleEndpoint(client, testURL) - // if err != nil { - // logrus.Fatalf("error response from test url: %s", err.Error()) - // } - // fmt.Println(resp) - //} + if keyPath != "" { + ts, err := profile.NewJWTProfileTokenSourceFromKeyFile(issuer, keyPath, scopes) + if err != nil { + logrus.Fatalf("error creating token source %s", err.Error()) + } + client = oauth2.NewClient(context.Background(), ts) + } http.HandleFunc("/jwt-profile", func(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { @@ -84,7 +78,7 @@ func main() { http.Error(w, err.Error(), http.StatusInternalServerError) return } - ts, err := profile.NewJWTProfileTokenSourceFromKeyFile(issuer, key, scopes) + ts, err := profile.NewJWTProfileTokenSourceFromKeyFileData(issuer, key, scopes) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -95,16 +89,6 @@ func main() { http.Error(w, err.Error(), http.StatusInternalServerError) return } - //assertion, err := oidc.NewJWTProfileAssertionFromFileData(key, []string{issuer}) - //if err != nil { - // http.Error(w, err.Error(), http.StatusInternalServerError) - // return - //} - //token, err := rp.JWTProfileAssertionExchange(ctx, assertion, scopes, provider) - //if err != nil { - // http.Error(w, err.Error(), http.StatusInternalServerError) - // return - //} data, err := json.Marshal(token) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/example/internal/mock/storage.go b/example/internal/mock/storage.go index 40a1f86..e04c045 100644 --- a/example/internal/mock/storage.go +++ b/example/internal/mock/storage.go @@ -210,23 +210,34 @@ func (s *AuthStorage) AuthorizeClientIDSecret(_ context.Context, id string, _ st return nil } -func (s *AuthStorage) GetUserinfoFromToken(ctx context.Context, _, _, _ string) (oidc.UserInfo, error) { - return s.GetUserinfoFromScopes(ctx, "", "", []string{}) +func (s *AuthStorage) SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, _, _, _ string) error { + return s.SetUserinfoFromScopes(ctx, userinfo, "", "", []string{}) } -func (s *AuthStorage) GetUserinfoFromScopes(_ context.Context, _, _ string, _ []string) (oidc.UserInfo, error) { - userinfo := oidc.NewUserInfo() +func (s *AuthStorage) SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, _, _ string, _ []string) error { userinfo.SetSubject(a.GetSubject()) userinfo.SetAddress(oidc.NewUserInfoAddress("Test 789\nPostfach 2", "", "", "", "", "")) userinfo.SetEmail("test", true) userinfo.SetPhone("0791234567", true) userinfo.SetName("Test") userinfo.AppendClaims("private_claim", "test") - return userinfo, nil + return nil } func (s *AuthStorage) GetPrivateClaimsFromScopes(_ context.Context, _, _ string, _ []string) (map[string]interface{}, error) { return map[string]interface{}{"private_claim": "test"}, nil } +func (s *AuthStorage) SetIntrospectionFromToken(ctx context.Context, userinfo oidc.IntrospectionResponse, tokenID, subject, clientID string) error { + if err := s.SetUserinfoFromScopes(ctx, userinfo, "", "", []string{}); err != nil { + return err + } + userinfo.SetClientID(a.ClientID) + return nil +} + +func (s *AuthStorage) ValidateJWTProfileScopes(ctx context.Context, userID string, scope oidc.Scopes) (oidc.Scopes, error) { + return scope, nil +} + type ConfClient struct { applicationType op.ApplicationType authMethod oidc.AuthMethod diff --git a/pkg/client/profile/jwt_profile.go b/pkg/client/profile/jwt_profile.go index d60a2f8..46a0fe9 100644 --- a/pkg/client/profile/jwt_profile.go +++ b/pkg/client/profile/jwt_profile.go @@ -23,7 +23,15 @@ type jwtProfileTokenSource struct { tokenEndpoint string } -func NewJWTProfileTokenSourceFromKeyFile(issuer string, data []byte, scopes []string, options ...func(source *jwtProfileTokenSource)) (oauth2.TokenSource, error) { +func NewJWTProfileTokenSourceFromKeyFile(issuer, keyPath string, scopes []string, options ...func(source *jwtProfileTokenSource)) (oauth2.TokenSource, error) { + keyData, err := client.ConfigFromKeyFile(keyPath) + if err != nil { + return nil, err + } + return NewJWTProfileTokenSource(issuer, keyData.UserID, keyData.KeyID, []byte(keyData.Key), scopes, options...) +} + +func NewJWTProfileTokenSourceFromKeyFileData(issuer string, data []byte, scopes []string, options ...func(source *jwtProfileTokenSource)) (oauth2.TokenSource, error) { keyData, err := client.ConfigFromKeyFileData(data) if err != nil { return nil, err diff --git a/pkg/oidc/introspection.go b/pkg/oidc/introspection.go index 1a66520..a2176aa 100644 --- a/pkg/oidc/introspection.go +++ b/pkg/oidc/introspection.go @@ -21,7 +21,7 @@ type IntrospectionResponse interface { UserInfoSetter SetActive(bool) IsActive() bool - SetScopes(scopes Scope) + SetScopes(scopes Scopes) SetClientID(id string) } @@ -31,7 +31,7 @@ func NewIntrospectionResponse() IntrospectionResponse { type introspectionResponse struct { Active bool `json:"active"` - Scope Scope `json:"scope,omitempty"` + Scope Scopes `json:"scope,omitempty"` ClientID string `json:"client_id,omitempty"` Subject string `json:"sub,omitempty"` userInfoProfile @@ -46,7 +46,7 @@ func (u *introspectionResponse) IsActive() bool { return u.Active } -func (u *introspectionResponse) SetScopes(scope Scope) { +func (u *introspectionResponse) SetScopes(scope Scopes) { u.Scope = scope } @@ -252,10 +252,6 @@ func (i *introspectionResponse) MarshalJSON() ([]byte, error) { } return json.Marshal(i.claims) - //if err != nil { - // return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims) - //} - //return utils.ConcatenateJSON(b, claims) } func (i *introspectionResponse) UnmarshalJSON(data []byte) error { diff --git a/pkg/oidc/types.go b/pkg/oidc/types.go index fd496da..5525923 100644 --- a/pkg/oidc/types.go +++ b/pkg/oidc/types.go @@ -59,7 +59,6 @@ type Prompt string type ResponseType string type Scopes []string -type Scope []string //TODO: hurst? func (s Scopes) Encode() string { return strings.Join(s, " ") @@ -74,16 +73,16 @@ func (s *Scopes) MarshalText() ([]byte, error) { return []byte(s.Encode()), nil } -func (s *Scope) MarshalJSON() ([]byte, error) { - return json.Marshal(Scopes(*s).Encode()) +func (s *Scopes) MarshalJSON() ([]byte, error) { + return json.Marshal((*s).Encode()) } -func (s *Scope) UnmarshalJSON(data []byte) error { +func (s *Scopes) UnmarshalJSON(data []byte) error { var str string if err := json.Unmarshal(data, &str); err != nil { return err } - *s = Scope(strings.Split(str, " ")) + *s = strings.Split(str, " ") return nil } diff --git a/pkg/oidc/userinfo.go b/pkg/oidc/userinfo.go index 3a92501..6bc0016 100644 --- a/pkg/oidc/userinfo.go +++ b/pkg/oidc/userinfo.go @@ -355,11 +355,6 @@ func (i *userinfo) MarshalJSON() ([]byte, error) { } return json.Marshal(i.claims) - //claims, err := json.Marshal(i.claims) - //if err != nil { - // return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims) - //} - //return utils.ConcatenateJSON(b, claims) } func (i *userinfo) UnmarshalJSON(data []byte) error { diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 291214c..d8ef7c3 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -21,20 +21,16 @@ func Discover(w http.ResponseWriter, config *oidc.DiscoveryConfiguration) { func CreateDiscoveryConfig(c Configuration, s Signer) *oidc.DiscoveryConfiguration { return &oidc.DiscoveryConfiguration{ - Issuer: c.Issuer(), - AuthorizationEndpoint: c.AuthorizationEndpoint().Absolute(c.Issuer()), - TokenEndpoint: c.TokenEndpoint().Absolute(c.Issuer()), - IntrospectionEndpoint: c.IntrospectionEndpoint().Absolute(c.Issuer()), - UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()), - //RevocationEndpoint: c.RevocationEndpoint().Absolute(c.Issuer()), - EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()), - // CheckSessionIframe: c.TokenEndpoint().Absolute(c.Issuer())(c.CheckSessionIframe), - JwksURI: c.KeysEndpoint().Absolute(c.Issuer()), - ScopesSupported: Scopes(c), - ResponseTypesSupported: ResponseTypes(c), - //ResponseModesSupported: - GrantTypesSupported: GrantTypes(c), - //ACRValuesSupported: ACRValues(c), + Issuer: c.Issuer(), + AuthorizationEndpoint: c.AuthorizationEndpoint().Absolute(c.Issuer()), + TokenEndpoint: c.TokenEndpoint().Absolute(c.Issuer()), + IntrospectionEndpoint: c.IntrospectionEndpoint().Absolute(c.Issuer()), + UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()), + EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()), + JwksURI: c.KeysEndpoint().Absolute(c.Issuer()), + ScopesSupported: Scopes(c), + ResponseTypesSupported: ResponseTypes(c), + GrantTypesSupported: GrantTypes(c), SubjectTypesSupported: SubjectTypes(c), IDTokenSigningAlgValuesSupported: SigAlgorithms(s), TokenEndpointAuthMethodsSupported: AuthMethodsTokenEndpoint(c), diff --git a/pkg/utils/http.go b/pkg/utils/http.go index 6f1b74d..fa51815 100644 --- a/pkg/utils/http.go +++ b/pkg/utils/http.go @@ -42,9 +42,6 @@ func FormRequest(endpoint string, request interface{}, encoder Encoder, authFn i if fn, ok := authFn.(FormAuthorization); ok { fn(form) } - if fn, ok := authFn.(func(url.Values)); ok { - fn(form) - } body := strings.NewReader(form.Encode()) req, err := http.NewRequest("POST", endpoint, body) if err != nil { From fb9d1b3c4a47e8e0341413d24c23eb5daf7c852f Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Fri, 12 Feb 2021 12:51:22 +0100 Subject: [PATCH 12/14] Update example/internal/mock/storage.go Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com> --- example/internal/mock/storage.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/example/internal/mock/storage.go b/example/internal/mock/storage.go index e04c045..8ce59de 100644 --- a/example/internal/mock/storage.go +++ b/example/internal/mock/storage.go @@ -226,11 +226,11 @@ func (s *AuthStorage) GetPrivateClaimsFromScopes(_ context.Context, _, _ string, return map[string]interface{}{"private_claim": "test"}, nil } -func (s *AuthStorage) SetIntrospectionFromToken(ctx context.Context, userinfo oidc.IntrospectionResponse, tokenID, subject, clientID string) error { - if err := s.SetUserinfoFromScopes(ctx, userinfo, "", "", []string{}); err != nil { +func (s *AuthStorage) SetIntrospectionFromToken(ctx context.Context, introspect oidc.IntrospectionResponse, tokenID, subject, clientID string) error { + if err := s.SetUserinfoFromScopes(ctx, introspect, "", "", []string{}); err != nil { return err } - userinfo.SetClientID(a.ClientID) + introspect.SetClientID(a.ClientID) return nil } From 5678693d44b8b1628a7be2e3dd2d02630c4325f1 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Fri, 12 Feb 2021 12:52:12 +0100 Subject: [PATCH 13/14] clenaup --- pkg/op/op.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pkg/op/op.go b/pkg/op/op.go index ee41630..26445c5 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -25,13 +25,6 @@ const ( defaultUserinfoEndpoint = "userinfo" defaultEndSessionEndpoint = "end_session" defaultKeysEndpoint = "keys" - - //AuthMethodBasic AuthMethod = "client_secret_basic" - //AuthMethodPost AuthMethod = "client_secret_post" - //AuthMethodNone AuthMethod = "none" - //AuthMethodPrivateKeyJWT AuthMethod = "private_key_jwt" - - //CodeMethodS256 = "S256" ) var ( From e1f0456228f9500bc0512f319623c5848ea58490 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Mon, 22 Feb 2021 14:57:15 +0100 Subject: [PATCH 14/14] merge --- pkg/op/mock/storage.mock.go | 50 ------------------------------------- 1 file changed, 50 deletions(-) diff --git a/pkg/op/mock/storage.mock.go b/pkg/op/mock/storage.mock.go index cf57014..280e8e6 100644 --- a/pkg/op/mock/storage.mock.go +++ b/pkg/op/mock/storage.mock.go @@ -226,56 +226,6 @@ func (mr *MockStorageMockRecorder) SaveAuthCode(arg0, arg1, arg2 interface{}) *g return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveAuthCode", reflect.TypeOf((*MockStorage)(nil).SaveAuthCode), arg0, arg1, arg2) } -// SaveNewKeyPair mocks base method -func (m *MockStorage) SaveNewKeyPair(arg0 context.Context) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SaveNewKeyPair", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// SetIntrospectionFromToken mocks base method -func (m *MockStorage) SetIntrospectionFromToken(arg0 context.Context, arg1 oidc.IntrospectionResponse, arg2, arg3, arg4 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetIntrospectionFromToken", arg0, arg1, arg2, arg3, arg4) - ret0, _ := ret[0].(error) - return ret0 -} - -// SetIntrospectionFromToken indicates an expected call of SetIntrospectionFromToken -func (mr *MockStorageMockRecorder) SetIntrospectionFromToken(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetIntrospectionFromToken", reflect.TypeOf((*MockStorage)(nil).SetIntrospectionFromToken), arg0, arg1, arg2, arg3, arg4) -} - -// SetUserinfoFromScopes mocks base method -func (m *MockStorage) SetUserinfoFromScopes(arg0 context.Context, arg1 oidc.UserInfoSetter, arg2, arg3 string, arg4 []string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetUserinfoFromScopes", arg0, arg1, arg2, arg3, arg4) - ret0, _ := ret[0].(error) - return ret0 -} - -// SetUserinfoFromScopes indicates an expected call of SetUserinfoFromScopes -func (mr *MockStorageMockRecorder) SetUserinfoFromScopes(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserinfoFromScopes", reflect.TypeOf((*MockStorage)(nil).SetUserinfoFromScopes), arg0, arg1, arg2, arg3, arg4) -} - -// SetUserinfoFromToken mocks base method -func (m *MockStorage) SetUserinfoFromToken(arg0 context.Context, arg1 oidc.UserInfoSetter, arg2, arg3, arg4 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetUserinfoFromToken", arg0, arg1, arg2, arg3, arg4) - ret0, _ := ret[0].(error) - return ret0 -} - -// SetUserinfoFromToken indicates an expected call of SetUserinfoFromToken -func (mr *MockStorageMockRecorder) SetUserinfoFromToken(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserinfoFromToken", reflect.TypeOf((*MockStorage)(nil).SetUserinfoFromToken), arg0, arg1, arg2, arg3, arg4) -} - // SetIntrospectionFromToken mocks base method func (m *MockStorage) SetIntrospectionFromToken(arg0 context.Context, arg1 oidc.IntrospectionResponse, arg2, arg3, arg4 string) error { m.ctrl.T.Helper()