claims assertion

This commit is contained in:
Livio Amstutz 2020-10-14 16:41:04 +02:00
parent d6203fb0d5
commit b8d892443c
9 changed files with 491 additions and 189 deletions

View file

@ -10,7 +10,9 @@ const (
ApplicationTypeWeb ApplicationType = iota
ApplicationTypeUserAgent
ApplicationTypeNative
)
const (
AccessTokenTypeBearer AccessTokenType = iota
AccessTokenTypeJWT
)
@ -33,6 +35,8 @@ type Client interface {
IDTokenLifetime() time.Duration
DevMode() bool
AllowedScopes() []string
AssertAdditionalIdTokenScopes() bool
AssertAdditionalAccessTokenScopes() bool
}
func ContainsResponseType(types []oidc.ResponseType, responseType oidc.ResponseType) bool {

View file

@ -77,6 +77,34 @@ 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()

View file

@ -171,6 +171,21 @@ func (mr *MockStorageMockRecorder) GetKeySet(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKeySet", reflect.TypeOf((*MockStorage)(nil).GetKeySet), arg0)
}
// GetPrivateClaimsFromScopes mocks base method
func (m *MockStorage) GetPrivateClaimsFromScopes(arg0 context.Context, arg1, arg2 string, arg3 []string) (map[string]interface{}, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPrivateClaimsFromScopes", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(map[string]interface{})
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetPrivateClaimsFromScopes indicates an expected call of GetPrivateClaimsFromScopes
func (mr *MockStorageMockRecorder) GetPrivateClaimsFromScopes(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPrivateClaimsFromScopes", reflect.TypeOf((*MockStorage)(nil).GetPrivateClaimsFromScopes), arg0, arg1, arg2, arg3)
}
// GetSigningKey mocks base method
func (m *MockStorage) GetSigningKey(arg0 context.Context, arg1 chan<- jose.SigningKey, arg2 chan<- error, arg3 <-chan time.Time) {
m.ctrl.T.Helper()
@ -184,10 +199,10 @@ func (mr *MockStorageMockRecorder) GetSigningKey(arg0, arg1, arg2, arg3 interfac
}
// GetUserinfoFromScopes mocks base method
func (m *MockStorage) GetUserinfoFromScopes(arg0 context.Context, arg1, arg2 string, arg3 []string) (oidc.UserInfoSetter, error) {
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.UserInfoSetter)
ret0, _ := ret[0].(oidc.UserInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@ -199,10 +214,10 @@ func (mr *MockStorageMockRecorder) GetUserinfoFromScopes(arg0, arg1, arg2, arg3
}
// GetUserinfoFromToken mocks base method
func (m *MockStorage) GetUserinfoFromToken(arg0 context.Context, arg1, arg2 string) (oidc.UserInfoSetter, error) {
func (m *MockStorage) GetUserinfoFromToken(arg0 context.Context, arg1, arg2 string) (oidc.UserInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetUserinfoFromToken", arg0, arg1, arg2)
ret0, _ := ret[0].(oidc.UserInfoSetter)
ret0, _ := ret[0].(oidc.UserInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}

View file

@ -51,6 +51,7 @@ type OpenIDProvider interface {
Encoder() utils.Encoder
IDTokenHintVerifier() IDTokenHintVerifier
JWTProfileVerifier() JWTProfileVerifier
AccessTokenVerifier() AccessTokenVerifier
Crypto() Crypto
DefaultLogoutRedirectURI() string
Signer() Signer
@ -152,6 +153,8 @@ type openidProvider struct {
signer Signer
idTokenHintVerifier IDTokenHintVerifier
jwtProfileVerifier JWTProfileVerifier
accessTokenVerifier AccessTokenVerifier
keySet *openIDKeySet
crypto Crypto
httpHandler http.Handler
decoder *schema.Decoder
@ -207,7 +210,7 @@ func (o *openidProvider) Encoder() utils.Encoder {
func (o *openidProvider) IDTokenHintVerifier() IDTokenHintVerifier {
if o.idTokenHintVerifier == nil {
o.idTokenHintVerifier = NewIDTokenHintVerifier(o.Issuer(), &openIDKeySet{o.Storage()})
o.idTokenHintVerifier = NewIDTokenHintVerifier(o.Issuer(), o.openIDKeySet())
}
return o.idTokenHintVerifier
}
@ -219,6 +222,20 @@ func (o *openidProvider) JWTProfileVerifier() JWTProfileVerifier {
return o.jwtProfileVerifier
}
func (o *openidProvider) AccessTokenVerifier() AccessTokenVerifier {
if o.accessTokenVerifier == nil {
o.accessTokenVerifier = NewAccessTokenVerifier(o.Issuer(), o.openIDKeySet())
}
return o.accessTokenVerifier
}
func (o *openidProvider) openIDKeySet() oidc.KeySet {
if o.keySet == nil {
o.keySet = &openIDKeySet{o.Storage()}
}
return o.keySet
}
func (o *openidProvider) Crypto() Crypto {
return o.crypto
}

View file

@ -28,8 +28,9 @@ type AuthStorage interface {
type OPStorage interface {
GetClientByClientID(context.Context, string) (Client, error)
AuthorizeClientIDSecret(context.Context, string, string) error
GetUserinfoFromScopes(context.Context, string, string, []string) (oidc.UserInfoSetter, error)
GetUserinfoFromToken(context.Context, string, string) (oidc.UserInfoSetter, error)
GetUserinfoFromScopes(context.Context, string, string, []string) (oidc.UserInfo, error)
GetUserinfoFromToken(context.Context, string, string) (oidc.UserInfo, error)
GetPrivateClaimsFromScopes(context.Context, string, string, []string) (map[string]interface{}, error)
GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error)
}

View file

@ -26,12 +26,12 @@ func CreateTokenResponse(ctx context.Context, authReq AuthRequest, client Client
var validity time.Duration
if createAccessToken {
var err error
accessToken, validity, err = CreateAccessToken(ctx, authReq, client.AccessTokenType(), creator)
accessToken, validity, err = CreateAccessToken(ctx, authReq, client.AccessTokenType(), creator, client)
if err != nil {
return nil, err
}
}
idToken, err := CreateIDToken(ctx, creator.Issuer(), authReq, client.IDTokenLifetime(), accessToken, code, creator.Storage(), creator.Signer())
idToken, err := CreateIDToken(ctx, creator.Issuer(), authReq, client.IDTokenLifetime(), accessToken, code, creator.Storage(), creator.Signer(), client.AssertAdditionalIdTokenScopes())
if err != nil {
return nil, err
}
@ -51,7 +51,7 @@ func CreateTokenResponse(ctx context.Context, authReq AuthRequest, client Client
}
func CreateJWTTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator) (*oidc.AccessTokenResponse, error) {
accessToken, validity, err := CreateAccessToken(ctx, tokenRequest, AccessTokenTypeBearer, creator)
accessToken, validity, err := CreateAccessToken(ctx, tokenRequest, AccessTokenTypeBearer, creator, nil)
if err != nil {
return nil, err
}
@ -64,14 +64,14 @@ func CreateJWTTokenResponse(ctx context.Context, tokenRequest TokenRequest, crea
}, nil
}
func CreateAccessToken(ctx context.Context, authReq TokenRequest, accessTokenType AccessTokenType, creator TokenCreator) (token string, validity time.Duration, err error) {
id, exp, err := creator.Storage().CreateToken(ctx, authReq)
func CreateAccessToken(ctx context.Context, tokenRequest TokenRequest, accessTokenType AccessTokenType, creator TokenCreator, client Client) (token string, validity time.Duration, err error) {
id, exp, err := creator.Storage().CreateToken(ctx, tokenRequest)
if err != nil {
return "", 0, err
}
validity = exp.Sub(time.Now().UTC())
if accessTokenType == AccessTokenTypeJWT {
token, err = CreateJWT(creator.Issuer(), authReq, exp, id, creator.Signer())
token, err = CreateJWT(ctx, creator.Issuer(), tokenRequest, exp, id, creator.Signer(), client, creator.Storage())
return
}
token, err = CreateBearerToken(id, creator.Crypto())
@ -82,14 +82,22 @@ func CreateBearerToken(id string, crypto Crypto) (string, error) {
return crypto.Encrypt(id)
}
func CreateJWT(issuer string, tokenRequest TokenRequest, exp time.Time, id string, signer Signer) (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()))
if err != nil {
return "", err
}
claims.SetPrivateClaims(privateClaims)
}
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) (string, error) {
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()
if accessToken != "" {
atHash, err := oidc.ClaimHash(accessToken, signer.SignatureAlgorithm())
@ -97,8 +105,13 @@ func CreateIDToken(ctx context.Context, issuer string, authReq AuthRequest, vali
return "", err
}
claims.SetAccessTokenHash(atHash)
} else {
userInfo, err := storage.GetUserinfoFromScopes(ctx, authReq.GetSubject(), authReq.GetClientID(), authReq.GetScopes())
scopes = removeUserinfoScopes(scopes)
}
if !additonalScopes {
scopes = removeAdditionalScopes(scopes)
}
if len(scopes) > 0 {
userInfo, err := storage.GetUserinfoFromScopes(ctx, authReq.GetSubject(), authReq.GetClientID(), scopes)
if err != nil {
return "", err
}
@ -114,3 +127,34 @@ func CreateIDToken(ctx context.Context, issuer string, authReq AuthRequest, vali
return utils.Sign(claims, signer.Signer())
}
func removeUserinfoScopes(scopes []string) []string {
for i := len(scopes) - 1; i >= 0; i-- {
if 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
}
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

@ -13,6 +13,7 @@ type UserinfoProvider interface {
Decoder() utils.Decoder
Crypto() Crypto
Storage() Storage
AccessTokenVerifier() AccessTokenVerifier
}
func userinfoHandler(userinfoProvider UserinfoProvider) func(http.ResponseWriter, *http.Request) {
@ -27,10 +28,20 @@ func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoP
http.Error(w, "access token missing", http.StatusUnauthorized)
return
}
tokenID, err := userinfoProvider.Crypto().Decrypt(accessToken)
if err != nil {
http.Error(w, "access token missing", http.StatusUnauthorized)
return
var tokenID string
if strings.HasPrefix(accessToken, "eyJhbGci") { //TODO: improve
accessTokenClaims, err := VerifyAccessToken(r.Context(), accessToken, userinfoProvider.AccessTokenVerifier())
if err != nil {
http.Error(w, "access token invalid", http.StatusUnauthorized)
return
}
tokenID = accessTokenClaims.GetTokenID()
} else {
tokenID, err = userinfoProvider.Crypto().Decrypt(accessToken)
if err != nil {
http.Error(w, "access token invalid", http.StatusUnauthorized)
return
}
}
info, err := userinfoProvider.Storage().GetUserinfoFromToken(r.Context(), tokenID, r.Header.Get("origin"))
if err != nil {

View file

@ -0,0 +1,85 @@
package op
import (
"context"
"time"
"github.com/caos/oidc/pkg/oidc"
)
type AccessTokenVerifier interface {
oidc.Verifier
SupportedSignAlgs() []string
KeySet() oidc.KeySet
}
type accessTokenVerifier struct {
issuer string
maxAgeIAT time.Duration
offset time.Duration
supportedSignAlgs []string
maxAge time.Duration
acr oidc.ACRVerifier
keySet oidc.KeySet
}
//Issuer implements oidc.Verifier interface
func (i *accessTokenVerifier) Issuer() string {
return i.issuer
}
//MaxAgeIAT implements oidc.Verifier interface
func (i *accessTokenVerifier) MaxAgeIAT() time.Duration {
return i.maxAgeIAT
}
//Offset implements oidc.Verifier interface
func (i *accessTokenVerifier) Offset() time.Duration {
return i.offset
}
//SupportedSignAlgs implements AccessTokenVerifier interface
func (i *accessTokenVerifier) SupportedSignAlgs() []string {
return i.supportedSignAlgs
}
//KeySet implements AccessTokenVerifier interface
func (i *accessTokenVerifier) KeySet() oidc.KeySet {
return i.keySet
}
func NewAccessTokenVerifier(issuer string, keySet oidc.KeySet) AccessTokenVerifier {
verifier := &idTokenHintVerifier{
issuer: issuer,
keySet: keySet,
}
return verifier
}
//VerifyAccessToken validates the access token (issuer, signature and expiration)
func VerifyAccessToken(ctx context.Context, token string, v AccessTokenVerifier) (oidc.AccessTokenClaims, error) {
claims := oidc.EmptyAccessTokenClaims()
decrypted, err := oidc.DecryptToken(token)
if err != nil {
return nil, err
}
payload, err := oidc.ParseToken(decrypted, claims)
if err != nil {
return nil, err
}
if err := oidc.CheckIssuer(claims, v.Issuer()); err != nil {
return nil, err
}
if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs(), v.KeySet()); err != nil {
return nil, err
}
if err = oidc.CheckExpiration(claims, v.Offset()); err != nil {
return nil, err
}
return claims, nil
}