Merge branch 'master' into signingkey

This commit is contained in:
Livio Amstutz 2020-12-16 08:01:37 +01:00
commit b2f23dc5b7
20 changed files with 240 additions and 134 deletions

View file

@ -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]

View file

@ -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 {

View file

@ -19,6 +19,8 @@ type Configuration interface {
AuthMethodPostSupported() bool
CodeMethodS256Supported() bool
GrantTypeTokenExchangeSupported() bool
GrantTypeJWTAuthorizationSupported() bool
}
func ValidateIssuer(issuer string) error {

View file

@ -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 {

View file

@ -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
}

View file

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

View file

@ -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()

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)