diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3d24fe9..79ff704 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,3 +9,7 @@ updates: commit-message: prefix: chore include: scope +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: weekly \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 18f5c01..49016bb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Setup go - uses: actions/setup-go@v2-beta + uses: actions/setup-go@v2 with: go-version: ${{ matrix.go }} - run: go test -race -v -coverprofile=profile.cov ./pkg/... @@ -25,9 +25,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Source checkout - uses: actions/checkout@v1 - with: - fetch-depth: 1 + uses: actions/checkout@v2 - name: Semantic Release uses: cycjimmy/semantic-release-action@v2 with: diff --git a/example/client/app/app.go b/example/client/app/app.go index 1c9c469..ad00adb 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -8,6 +8,7 @@ import ( "io/ioutil" "net/http" "os" + "strings" "time" "github.com/google/uuid" @@ -28,11 +29,11 @@ func main() { clientSecret := os.Getenv("CLIENT_SECRET") issuer := os.Getenv("ISSUER") port := os.Getenv("PORT") + scopes := strings.Split(os.Getenv("SCOPES"), " ") ctx := context.Background() redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath) - scopes := []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopeAddress} cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure()) provider, err := rp.NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes, rp.WithPKCE(cookieHandler), diff --git a/example/internal/mock/storage.go b/example/internal/mock/storage.go index ffddd28..bc097c0 100644 --- a/example/internal/mock/storage.go +++ b/example/internal/mock/storage.go @@ -281,10 +281,26 @@ func (c *ConfClient) AllowedScopes() []string { return nil } -func (c *ConfClient) AssertAdditionalIdTokenScopes() bool { +func (c *ConfClient) RestrictAdditionalIdTokenScopes() func(scopes []string) []string { + return func(scopes []string) []string { + return scopes + } +} + +func (c *ConfClient) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string { + return func(scopes []string) []string { + return scopes + } +} + +func (c *ConfClient) IsScopeAllowed(scope string) bool { return false } -func (c *ConfClient) AssertAdditionalAccessTokenScopes() bool { +func (c *ConfClient) IDTokenUserinfoClaimsAssertion() bool { return false } + +func (c *ConfClient) ClockSkew() time.Duration { + return 0 +} diff --git a/example/server/default/default.go b/example/server/default/default.go index d5922d4..7edaf2e 100644 --- a/example/server/default/default.go +++ b/example/server/default/default.go @@ -68,5 +68,5 @@ func HandleLogin(w http.ResponseWriter, r *http.Request) { func HandleCallback(w http.ResponseWriter, r *http.Request) { r.ParseForm() client := r.FormValue("client") - http.Redirect(w, r, "/authorize/"+client, http.StatusFound) + http.Redirect(w, r, "/authorize/callback?id="+client, http.StatusFound) } diff --git a/go.mod b/go.mod index 4112c69..9c46198 100644 --- a/go.mod +++ b/go.mod @@ -15,9 +15,9 @@ require ( github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/sirupsen/logrus v1.7.0 github.com/stretchr/testify v1.6.1 - golang.org/x/net v0.0.0-20200904194848-62affa334b73 + golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 - golang.org/x/text v0.3.3 + golang.org/x/text v0.3.4 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect gopkg.in/square/go-jose.v2 v2.5.1 ) diff --git a/go.sum b/go.sum index b2b0f45..6eedcb0 100644 --- a/go.sum +++ b/go.sum @@ -126,8 +126,6 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -141,8 +139,6 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -280,6 +276,8 @@ golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index 99f18c7..ff8f33e 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -48,8 +48,11 @@ func EmptyAccessTokenClaims() AccessTokenClaims { return new(accessTokenClaims) } -func NewAccessTokenClaims(issuer, subject string, audience []string, expiration time.Time, id string) AccessTokenClaims { - now := time.Now().UTC() +func NewAccessTokenClaims(issuer, subject string, audience []string, expiration time.Time, id, clientID string, skew time.Duration) AccessTokenClaims { + now := time.Now().UTC().Add(-skew) + if len(audience) == 0 { + audience = append(audience, clientID) + } return &accessTokenClaims{ Issuer: issuer, Subject: subject, @@ -200,13 +203,14 @@ func EmptyIDTokenClaims() IDTokenClaims { return new(idTokenClaims) } -func NewIDTokenClaims(issuer, subject string, audience []string, expiration, authTime time.Time, nonce string, acr string, amr []string, clientID string) IDTokenClaims { +func NewIDTokenClaims(issuer, subject string, audience []string, expiration, authTime time.Time, nonce string, acr string, amr []string, clientID string, skew time.Duration) IDTokenClaims { + audience = AppendClientIDToAudience(clientID, audience) return &idTokenClaims{ Issuer: issuer, Audience: audience, Expiration: Time(expiration), - IssuedAt: Time(time.Now().UTC()), - AuthTime: Time(authTime), + IssuedAt: Time(time.Now().UTC().Add(-skew)), + AuthTime: Time(authTime.Add(-skew)), Nonce: nonce, AuthenticationContextClassReference: acr, AuthenticationMethodsReferences: amr, @@ -441,3 +445,12 @@ func ClaimHash(claim string, sigAlgorithm jose.SignatureAlgorithm) (string, erro return utils.HashString(hash, claim, true), nil } + +func AppendClientIDToAudience(clientID string, audience []string) []string { + for _, aud := range audience { + if aud == clientID { + return audience + } + } + return append(audience, clientID) +} diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go index e80d28a..1312b18 100644 --- a/pkg/oidc/token_request.go +++ b/pkg/oidc/token_request.go @@ -9,8 +9,12 @@ import ( const ( //GrantTypeCode defines the grant_type `authorization_code` used for the Token Request in the Authorization Code Flow GrantTypeCode GrantType = "authorization_code" - //GrantTypeBearer define the grant_type `urn:ietf:params:oauth:grant-type:jwt-bearer` used for the JWT Authorization Grant + + //GrantTypeBearer defines the grant_type `urn:ietf:params:oauth:grant-type:jwt-bearer` used for the JWT Authorization Grant GrantTypeBearer GrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer" + + //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" ) type GrantType string diff --git a/pkg/op/authrequest.go b/pkg/op/authrequest.go index 4d6118c..9e320f8 100644 --- a/pkg/op/authrequest.go +++ b/pkg/op/authrequest.go @@ -121,7 +121,7 @@ func ValidateAuthReqScopes(client Client, scopes []string) ([]string, error) { scope == oidc.ScopePhone || scope == oidc.ScopeAddress || scope == oidc.ScopeOfflineAccess) && - !utils.Contains(client.AllowedScopes(), scope) { + !client.IsScopeAllowed(scope) { scopes[i] = scopes[len(scopes)-1] scopes[len(scopes)-1] = "" scopes = scopes[:len(scopes)-1] diff --git a/pkg/op/client.go b/pkg/op/client.go index 790933e..6d0891c 100644 --- a/pkg/op/client.go +++ b/pkg/op/client.go @@ -34,9 +34,11 @@ type Client interface { AccessTokenType() AccessTokenType IDTokenLifetime() time.Duration DevMode() bool - AllowedScopes() []string - AssertAdditionalIdTokenScopes() bool - AssertAdditionalAccessTokenScopes() bool + RestrictAdditionalIdTokenScopes() func(scopes []string) []string + RestrictAdditionalAccessTokenScopes() func(scopes []string) []string + IsScopeAllowed(scope string) bool + IDTokenUserinfoClaimsAssertion() bool + ClockSkew() time.Duration } func ContainsResponseType(types []oidc.ResponseType, responseType oidc.ResponseType) bool { diff --git a/pkg/op/config.go b/pkg/op/config.go index d64c0ee..a2b831e 100644 --- a/pkg/op/config.go +++ b/pkg/op/config.go @@ -19,6 +19,8 @@ type Configuration interface { AuthMethodPostSupported() bool CodeMethodS256Supported() bool + GrantTypeTokenExchangeSupported() bool + GrantTypeJWTAuthorizationSupported() bool } func ValidateIssuer(issuer string) error { diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 7611090..4bc1272 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -52,22 +52,23 @@ func Scopes(c Configuration) []string { func ResponseTypes(c Configuration) []string { return []string{ - "code", - "id_token", - // "code token", - // "code id_token", - "id_token token", - // "code id_token token" - } + string(oidc.ResponseTypeCode), + string(oidc.ResponseTypeIDTokenOnly), + string(oidc.ResponseTypeIDToken), + } //TODO: ok for now, check later if dynamic needed } func GrantTypes(c Configuration) []string { - return []string{ - "client_credentials", - "authorization_code", - // "password", - "urn:ietf:params:oauth:grant-type:token-exchange", + grantTypes := []string{ + string(oidc.GrantTypeCode), } + if c.GrantTypeTokenExchangeSupported() { + grantTypes = append(grantTypes, string(oidc.GrantTypeTokenExchange)) + } + if c.GrantTypeJWTAuthorizationSupported() { + grantTypes = append(grantTypes, string(oidc.GrantTypeBearer)) + } + return grantTypes } func SupportedClaims(c Configuration) []string { diff --git a/pkg/op/mock/client.go b/pkg/op/mock/client.go index 12c00cc..b7ac3e8 100644 --- a/pkg/op/mock/client.go +++ b/pkg/op/mock/client.go @@ -26,7 +26,7 @@ func NewClientExpectAny(t *testing.T, appType op.ApplicationType) op.Client { func(id string) string { return "login?id=" + id }) - m.EXPECT().AllowedScopes().AnyTimes().Return(nil) + m.EXPECT().IsScopeAllowed(gomock.Any()).AnyTimes().Return(false) return c } diff --git a/pkg/op/mock/client.mock.go b/pkg/op/mock/client.mock.go index 0780623..1a15624 100644 --- a/pkg/op/mock/client.mock.go +++ b/pkg/op/mock/client.mock.go @@ -49,20 +49,6 @@ func (mr *MockClientMockRecorder) AccessTokenType() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccessTokenType", reflect.TypeOf((*MockClient)(nil).AccessTokenType)) } -// AllowedScopes mocks base method -func (m *MockClient) AllowedScopes() []string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AllowedScopes") - ret0, _ := ret[0].([]string) - return ret0 -} - -// AllowedScopes indicates an expected call of AllowedScopes -func (mr *MockClientMockRecorder) AllowedScopes() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllowedScopes", reflect.TypeOf((*MockClient)(nil).AllowedScopes)) -} - // ApplicationType mocks base method func (m *MockClient) ApplicationType() op.ApplicationType { m.ctrl.T.Helper() @@ -77,34 +63,6 @@ func (mr *MockClientMockRecorder) ApplicationType() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplicationType", reflect.TypeOf((*MockClient)(nil).ApplicationType)) } -// AssertAdditionalAccessTokenScopes mocks base method -func (m *MockClient) AssertAdditionalAccessTokenScopes() bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AssertAdditionalAccessTokenScopes") - ret0, _ := ret[0].(bool) - return ret0 -} - -// AssertAdditionalAccessTokenScopes indicates an expected call of AssertAdditionalAccessTokenScopes -func (mr *MockClientMockRecorder) AssertAdditionalAccessTokenScopes() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssertAdditionalAccessTokenScopes", reflect.TypeOf((*MockClient)(nil).AssertAdditionalAccessTokenScopes)) -} - -// AssertAdditionalIdTokenScopes mocks base method -func (m *MockClient) AssertAdditionalIdTokenScopes() bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AssertAdditionalIdTokenScopes") - ret0, _ := ret[0].(bool) - return ret0 -} - -// AssertAdditionalIdTokenScopes indicates an expected call of AssertAdditionalIdTokenScopes -func (mr *MockClientMockRecorder) AssertAdditionalIdTokenScopes() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssertAdditionalIdTokenScopes", reflect.TypeOf((*MockClient)(nil).AssertAdditionalIdTokenScopes)) -} - // AuthMethod mocks base method func (m *MockClient) AuthMethod() op.AuthMethod { m.ctrl.T.Helper() @@ -119,6 +77,20 @@ func (mr *MockClientMockRecorder) AuthMethod() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthMethod", reflect.TypeOf((*MockClient)(nil).AuthMethod)) } +// ClockSkew mocks base method +func (m *MockClient) ClockSkew() time.Duration { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ClockSkew") + ret0, _ := ret[0].(time.Duration) + return ret0 +} + +// ClockSkew indicates an expected call of ClockSkew +func (mr *MockClientMockRecorder) ClockSkew() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClockSkew", reflect.TypeOf((*MockClient)(nil).ClockSkew)) +} + // DevMode mocks base method func (m *MockClient) DevMode() bool { m.ctrl.T.Helper() @@ -161,6 +133,34 @@ func (mr *MockClientMockRecorder) IDTokenLifetime() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDTokenLifetime", reflect.TypeOf((*MockClient)(nil).IDTokenLifetime)) } +// IDTokenUserinfoClaimsAssertion mocks base method +func (m *MockClient) IDTokenUserinfoClaimsAssertion() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IDTokenUserinfoClaimsAssertion") + ret0, _ := ret[0].(bool) + return ret0 +} + +// IDTokenUserinfoClaimsAssertion indicates an expected call of IDTokenUserinfoClaimsAssertion +func (mr *MockClientMockRecorder) IDTokenUserinfoClaimsAssertion() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDTokenUserinfoClaimsAssertion", reflect.TypeOf((*MockClient)(nil).IDTokenUserinfoClaimsAssertion)) +} + +// IsScopeAllowed mocks base method +func (m *MockClient) IsScopeAllowed(arg0 string) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsScopeAllowed", arg0) + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsScopeAllowed indicates an expected call of IsScopeAllowed +func (mr *MockClientMockRecorder) IsScopeAllowed(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsScopeAllowed", reflect.TypeOf((*MockClient)(nil).IsScopeAllowed), arg0) +} + // LoginURL mocks base method func (m *MockClient) LoginURL(arg0 string) string { m.ctrl.T.Helper() @@ -216,3 +216,31 @@ func (mr *MockClientMockRecorder) ResponseTypes() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResponseTypes", reflect.TypeOf((*MockClient)(nil).ResponseTypes)) } + +// RestrictAdditionalAccessTokenScopes mocks base method +func (m *MockClient) RestrictAdditionalAccessTokenScopes() func([]string) []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RestrictAdditionalAccessTokenScopes") + ret0, _ := ret[0].(func([]string) []string) + return ret0 +} + +// RestrictAdditionalAccessTokenScopes indicates an expected call of RestrictAdditionalAccessTokenScopes +func (mr *MockClientMockRecorder) RestrictAdditionalAccessTokenScopes() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestrictAdditionalAccessTokenScopes", reflect.TypeOf((*MockClient)(nil).RestrictAdditionalAccessTokenScopes)) +} + +// RestrictAdditionalIdTokenScopes mocks base method +func (m *MockClient) RestrictAdditionalIdTokenScopes() func([]string) []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RestrictAdditionalIdTokenScopes") + ret0, _ := ret[0].(func([]string) []string) + return ret0 +} + +// RestrictAdditionalIdTokenScopes indicates an expected call of RestrictAdditionalIdTokenScopes +func (mr *MockClientMockRecorder) RestrictAdditionalIdTokenScopes() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestrictAdditionalIdTokenScopes", reflect.TypeOf((*MockClient)(nil).RestrictAdditionalIdTokenScopes)) +} diff --git a/pkg/op/mock/configuration.mock.go b/pkg/op/mock/configuration.mock.go index 88e9aa7..ece747c 100644 --- a/pkg/op/mock/configuration.mock.go +++ b/pkg/op/mock/configuration.mock.go @@ -89,6 +89,34 @@ func (mr *MockConfigurationMockRecorder) EndSessionEndpoint() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EndSessionEndpoint", reflect.TypeOf((*MockConfiguration)(nil).EndSessionEndpoint)) } +// GrantTypeJWTAuthorizationSupported mocks base method +func (m *MockConfiguration) GrantTypeJWTAuthorizationSupported() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GrantTypeJWTAuthorizationSupported") + ret0, _ := ret[0].(bool) + return ret0 +} + +// GrantTypeJWTAuthorizationSupported indicates an expected call of GrantTypeJWTAuthorizationSupported +func (mr *MockConfigurationMockRecorder) GrantTypeJWTAuthorizationSupported() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypeJWTAuthorizationSupported", reflect.TypeOf((*MockConfiguration)(nil).GrantTypeJWTAuthorizationSupported)) +} + +// GrantTypeTokenExchangeSupported mocks base method +func (m *MockConfiguration) GrantTypeTokenExchangeSupported() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GrantTypeTokenExchangeSupported") + ret0, _ := ret[0].(bool) + return ret0 +} + +// GrantTypeTokenExchangeSupported indicates an expected call of GrantTypeTokenExchangeSupported +func (mr *MockConfigurationMockRecorder) GrantTypeTokenExchangeSupported() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypeTokenExchangeSupported", reflect.TypeOf((*MockConfiguration)(nil).GrantTypeTokenExchangeSupported)) +} + // Issuer mocks base method func (m *MockConfiguration) Issuer() string { m.ctrl.T.Helper() diff --git a/pkg/op/mock/storage.mock.impl.go b/pkg/op/mock/storage.mock.impl.go index 441d4a0..cd061a3 100644 --- a/pkg/op/mock/storage.mock.impl.go +++ b/pkg/op/mock/storage.mock.impl.go @@ -156,9 +156,24 @@ func (c *ConfClient) DevMode() bool { func (c *ConfClient) AllowedScopes() []string { return nil } -func (c *ConfClient) AssertAdditionalIdTokenScopes() bool { +func (c *ConfClient) RestrictAdditionalIdTokenScopes() func(scopes []string) []string { + return func(scopes []string) []string { + return scopes + } +} +func (c *ConfClient) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string { + return func(scopes []string) []string { + return scopes + } +} +func (c *ConfClient) IsScopeAllowed(scope string) bool { return false } -func (c *ConfClient) AssertAdditionalAccessTokenScopes() bool { + +func (c *ConfClient) IDTokenUserinfoClaimsAssertion() bool { return false } + +func (c *ConfClient) ClockSkew() time.Duration { + return 0 +} diff --git a/pkg/op/op.go b/pkg/op/op.go index 1a93024..1d323c3 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -49,7 +49,6 @@ type OpenIDProvider interface { Decoder() utils.Decoder Encoder() utils.Encoder IDTokenHintVerifier() IDTokenHintVerifier - JWTProfileVerifier() JWTProfileVerifier AccessTokenVerifier() AccessTokenVerifier Crypto() Crypto DefaultLogoutRedirectURI() string @@ -76,7 +75,7 @@ func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router router.HandleFunc(readinessEndpoint, readyHandler(o.Probes())) router.HandleFunc(oidc.DiscoveryEndpoint, discoveryHandler(o, o.Signer())) router.Handle(o.AuthorizationEndpoint().Relative(), intercept(authorizeHandler(o))) - router.Handle(o.AuthorizationEndpoint().Relative()+"/{id}", intercept(authorizeCallbackHandler(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.UserinfoEndpoint().Relative(), userinfoHandler(o)) router.Handle(o.EndSessionEndpoint().Relative(), intercept(endSessionHandler(o))) @@ -89,15 +88,6 @@ type Config struct { CryptoKey [32]byte DefaultLogoutRedirectURI string CodeMethodS256 bool - - //TODO: add to config after updating Configuration interface for DiscoveryConfig - // ScopesSupported: oidc.SupportedScopes, - // ResponseTypesSupported: responseTypes, - // GrantTypesSupported: oidc.SupportedGrantTypes, - // ClaimsSupported: oidc.SupportedClaims, - // IdTokenSigningAlgValuesSupported: []string{keys.SigningAlgorithm}, - // SubjectTypesSupported: []string{"public"}, - // TokenEndpointAuthMethodsSupported: } type endpoints struct { @@ -195,6 +185,14 @@ func (o *openidProvider) CodeMethodS256Supported() bool { return o.config.CodeMethodS256 } +func (o *openidProvider) GrantTypeTokenExchangeSupported() bool { + return false +} + +func (o *openidProvider) GrantTypeJWTAuthorizationSupported() bool { + return true +} + func (o *openidProvider) Storage() Storage { return o.storage } diff --git a/pkg/op/token.go b/pkg/op/token.go index 2d66ef5..fe6658a 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -31,7 +31,7 @@ func CreateTokenResponse(ctx context.Context, authReq AuthRequest, client Client return nil, err } } - idToken, err := CreateIDToken(ctx, creator.Issuer(), authReq, client.IDTokenLifetime(), accessToken, code, creator.Storage(), creator.Signer(), client.AssertAdditionalIdTokenScopes()) + idToken, err := CreateIDToken(ctx, creator.Issuer(), authReq, client.IDTokenLifetime(), accessToken, code, creator.Storage(), creator.Signer(), client) if err != nil { return nil, err } @@ -69,7 +69,7 @@ func CreateAccessToken(ctx context.Context, tokenRequest TokenRequest, accessTok if err != nil { return "", 0, err } - validity = exp.Sub(time.Now().UTC()) + validity = exp.Add(client.ClockSkew()).Sub(time.Now().UTC()) if accessTokenType == AccessTokenTypeJWT { token, err = CreateJWT(ctx, creator.Issuer(), tokenRequest, exp, id, creator.Signer(), client, creator.Storage()) return @@ -83,9 +83,10 @@ func CreateBearerToken(tokenID, subject string, crypto Crypto) (string, error) { } func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, exp time.Time, id string, signer Signer, client Client, storage Storage) (string, error) { - claims := oidc.NewAccessTokenClaims(issuer, tokenRequest.GetSubject(), tokenRequest.GetAudience(), exp, id) - if client != nil && client.AssertAdditionalAccessTokenScopes() { - privateClaims, err := storage.GetPrivateClaimsFromScopes(ctx, tokenRequest.GetSubject(), client.GetID(), removeUserinfoScopes(tokenRequest.GetScopes())) + claims := oidc.NewAccessTokenClaims(issuer, tokenRequest.GetSubject(), tokenRequest.GetAudience(), exp, id, client.GetID(), client.ClockSkew()) + if client != nil { + restrictedScopes := client.RestrictAdditionalAccessTokenScopes()(tokenRequest.GetScopes()) + privateClaims, err := storage.GetPrivateClaimsFromScopes(ctx, tokenRequest.GetSubject(), client.GetID(), removeUserinfoScopes(restrictedScopes)) if err != nil { return "", err } @@ -94,21 +95,19 @@ func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, ex return utils.Sign(claims, signer.Signer()) } -func CreateIDToken(ctx context.Context, issuer string, authReq AuthRequest, validity time.Duration, accessToken, code string, storage Storage, signer Signer, additonalScopes bool) (string, error) { - exp := time.Now().UTC().Add(validity) - claims := oidc.NewIDTokenClaims(issuer, authReq.GetSubject(), authReq.GetAudience(), exp, authReq.GetAuthTime(), authReq.GetNonce(), authReq.GetACR(), authReq.GetAMR(), authReq.GetClientID()) - scopes := authReq.GetScopes() - +func CreateIDToken(ctx context.Context, issuer string, authReq AuthRequest, validity time.Duration, accessToken, code string, storage Storage, signer Signer, client Client) (string, error) { + exp := time.Now().UTC().Add(client.ClockSkew()).Add(validity) + claims := oidc.NewIDTokenClaims(issuer, authReq.GetSubject(), authReq.GetAudience(), exp, authReq.GetAuthTime(), authReq.GetNonce(), authReq.GetACR(), authReq.GetAMR(), authReq.GetClientID(), client.ClockSkew()) + scopes := client.RestrictAdditionalIdTokenScopes()(authReq.GetScopes()) if accessToken != "" { atHash, err := oidc.ClaimHash(accessToken, signer.SignatureAlgorithm()) if err != nil { return "", err } claims.SetAccessTokenHash(atHash) - scopes = removeUserinfoScopes(scopes) - } - if !additonalScopes { - scopes = removeAdditionalScopes(scopes) + if !client.IDTokenUserinfoClaimsAssertion() { + scopes = removeUserinfoScopes(scopes) + } } if len(scopes) > 0 { userInfo, err := storage.GetUserinfoFromScopes(ctx, authReq.GetSubject(), authReq.GetClientID(), scopes) @@ -142,19 +141,3 @@ func removeUserinfoScopes(scopes []string) []string { } return scopes } - -func removeAdditionalScopes(scopes []string) []string { - for i := len(scopes) - 1; i >= 0; i-- { - if !(scopes[i] == oidc.ScopeOpenID || - scopes[i] == oidc.ScopeProfile || - scopes[i] == oidc.ScopeEmail || - scopes[i] == oidc.ScopeAddress || - scopes[i] == oidc.ScopePhone) { - - scopes[i] = scopes[len(scopes)-1] - scopes[len(scopes)-1] = "" - scopes = scopes[:len(scopes)-1] - } - } - return scopes -} diff --git a/pkg/op/tokenrequest.go b/pkg/op/tokenrequest.go index cba70f3..c3860ff 100644 --- a/pkg/op/tokenrequest.go +++ b/pkg/op/tokenrequest.go @@ -4,6 +4,7 @@ import ( "context" "errors" "net/http" + "net/url" "github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/oidc/grants/tokenexchange" @@ -17,6 +18,12 @@ type Exchanger interface { Signer() Signer Crypto() Crypto AuthMethodPostSupported() bool + GrantTypeTokenExchangeSupported() bool + GrantTypeJWTAuthorizationSupported() bool +} + +type JWTAuthorizationGrantExchanger interface { + Exchanger JWTProfileVerifier() JWTProfileVerifier } @@ -27,17 +34,20 @@ func tokenHandler(exchanger Exchanger) func(w http.ResponseWriter, r *http.Reque CodeExchange(w, r, exchanger) return case string(oidc.GrantTypeBearer): - JWTProfile(w, r, exchanger) - return - case "exchange": - TokenExchange(w, r, exchanger) + if ex, ok := exchanger.(JWTAuthorizationGrantExchanger); ok && exchanger.GrantTypeJWTAuthorizationSupported() { + JWTProfile(w, r, ex) + return + } + case string(oidc.GrantTypeTokenExchange): + if exchanger.GrantTypeTokenExchangeSupported() { + TokenExchange(w, r, exchanger) + return + } case "": RequestError(w, r, ErrInvalidRequest("grant_type missing")) return - default: - RequestError(w, r, ErrInvalidRequest("grant_type not supported")) - return } + RequestError(w, r, ErrInvalidRequest("grant_type not supported")) } } @@ -75,9 +85,14 @@ func ParseAccessTokenRequest(r *http.Request, decoder utils.Decoder) (*oidc.Acce } clientID, clientSecret, ok := r.BasicAuth() if ok { - tokenReq.ClientID = clientID - tokenReq.ClientSecret = clientSecret - + tokenReq.ClientID, err = url.QueryUnescape(clientID) + if err != nil { + return nil, ErrInvalidRequest("invalid basic auth header") + } + tokenReq.ClientSecret, err = url.QueryUnescape(clientSecret) + if err != nil { + return nil, ErrInvalidRequest("invalid basic auth header") + } } return tokenReq, nil } @@ -106,7 +121,7 @@ func AuthorizeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exc return authReq, client, err } if client.AuthMethod() == AuthMethodPost && !exchanger.AuthMethodPostSupported() { - return nil, nil, errors.New("basic not supported") + return nil, nil, errors.New("auth_method post not supported") } err = AuthorizeClientIDSecret(ctx, tokenReq.ClientID, tokenReq.ClientSecret, exchanger.Storage()) if err != nil { @@ -137,7 +152,7 @@ func AuthorizeCodeChallenge(ctx context.Context, tokenReq *oidc.AccessTokenReque return authReq, nil } -func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger Exchanger) { +func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger JWTAuthorizationGrantExchanger) { profileRequest, err := ParseJWTProfileRequest(r, exchanger.Decoder()) if err != nil { RequestError(w, r, err)