From 2b9f7dfd189469d447098e757c829b84a817ef4c Mon Sep 17 00:00:00 2001 From: livio-a Date: Fri, 6 Mar 2020 17:14:30 +0100 Subject: [PATCH] Fix: userinfo (#15) * add idea to gitignore * working userinfo * cleanup * tests --- .gitignore | 1 + example/internal/mock/storage.go | 5 +- pkg/oidc/token.go | 59 +++++++++++- pkg/oidc/userinfo.go | 160 ++++++++++++++++++++----------- pkg/op/mock/storage.mock.go | 23 ++++- pkg/op/storage.go | 3 +- pkg/op/token.go | 10 +- pkg/op/userinfo.go | 37 ++++++- 8 files changed, 225 insertions(+), 73 deletions(-) diff --git a/.gitignore b/.gitignore index f94a21c..28e5e11 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ **/__debug_bin .vscode .DS_Store +.idea diff --git a/example/internal/mock/storage.go b/example/internal/mock/storage.go index 23a3394..c5797db 100644 --- a/example/internal/mock/storage.go +++ b/example/internal/mock/storage.go @@ -191,7 +191,10 @@ func (s *AuthStorage) AuthorizeClientIDSecret(_ context.Context, id string, _ st return nil } -func (s *AuthStorage) GetUserinfoFromScopes(context.Context, []string) (*oidc.Userinfo, error) { +func (s *AuthStorage) GetUserinfoFromToken(ctx context.Context, _ string) (*oidc.Userinfo, error) { + return s.GetUserinfoFromScopes(ctx, "", []string{}) +} +func (s *AuthStorage) GetUserinfoFromScopes(_ context.Context, _ string, _ []string) (*oidc.Userinfo, error) { return &oidc.Userinfo{ Subject: a.GetSubject(), Address: &oidc.UserinfoAddress{ diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index cde0885..8f2afc2 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -7,6 +7,7 @@ import ( "github.com/caos/oidc/pkg/utils" "golang.org/x/oauth2" + "golang.org/x/text/language" "gopkg.in/square/go-jose.v2" ) @@ -38,7 +39,6 @@ type AccessTokenClaims struct { type IDTokenClaims struct { Issuer string - Subject string Audiences []string Expiration time.Time NotBefore time.Time @@ -53,6 +53,7 @@ type IDTokenClaims struct { AuthenticationContextClassReference string AuthenticationMethodsReferences []string ClientID string + Userinfo Signature jose.SignatureAlgorithm //TODO: ??? } @@ -65,7 +66,6 @@ type jsonToken struct { NotBefore int64 `json:"nbf,omitempty"` IssuedAt int64 `json:"iat,omitempty"` JWTID string `json:"jti,omitempty"` - UpdatedAt int64 `json:"updated_at,omitempty"` AuthorizedParty string `json:"azp,omitempty"` Nonce string `json:"nonce,omitempty"` AuthTime int64 `json:"auth_time,omitempty"` @@ -79,6 +79,7 @@ type jsonToken struct { ClientID string `json:"client_id,omitempty"` AuthorizedActor interface{} `json:"may_act,omitempty"` //TODO: impl AccessTokenUseNumber int `json:"at_use_nbr,omitempty"` + jsonUserinfo } func (t *AccessTokenClaims) MarshalJSON() ([]byte, error) { @@ -142,7 +143,6 @@ func (t *IDTokenClaims) MarshalJSON() ([]byte, error) { NotBefore: timeToJSON(t.NotBefore), IssuedAt: timeToJSON(t.IssuedAt), JWTID: t.JWTID, - UpdatedAt: timeToJSON(t.UpdatedAt), AuthorizedParty: t.AuthorizedParty, Nonce: t.Nonce, AuthTime: timeToJSON(t.AuthTime), @@ -152,6 +152,7 @@ func (t *IDTokenClaims) MarshalJSON() ([]byte, error) { AuthenticationMethodsReferences: t.AuthenticationMethodsReferences, ClientID: t.ClientID, } + j.setUserinfo(t.Userinfo) return json.Marshal(j) } @@ -176,9 +177,61 @@ func (t *IDTokenClaims) UnmarshalJSON(b []byte) error { t.AuthorizedParty = i.AuthorizedParty t.AccessTokenHash = i.AccessTokenHash t.CodeHash = i.CodeHash + t.UserinfoProfile = i.UnmarshalUserinfoProfile() + t.UserinfoEmail = i.UnmarshalUserinfoEmail() + t.UserinfoPhone = i.UnmarshalUserinfoPhone() + t.Address = i.UnmarshalUserinfoAddress() return nil } +func (j *jsonToken) UnmarshalUserinfoProfile() UserinfoProfile { + locale, _ := language.Parse(j.Locale) + return UserinfoProfile{ + Name: j.Name, + GivenName: j.GivenName, + FamilyName: j.FamilyName, + MiddleName: j.MiddleName, + Nickname: j.Nickname, + Profile: j.Profile, + Picture: j.Picture, + Website: j.Website, + Gender: Gender(j.Gender), + Birthdate: j.Birthdate, + Zoneinfo: j.Zoneinfo, + Locale: locale, + UpdatedAt: time.Unix(j.UpdatedAt, 0).UTC(), + PreferredUsername: j.PreferredUsername, + } +} + +func (j *jsonToken) UnmarshalUserinfoEmail() UserinfoEmail { + return UserinfoEmail{ + Email: j.Email, + EmailVerified: j.EmailVerified, + } +} + +func (j *jsonToken) UnmarshalUserinfoPhone() UserinfoPhone { + return UserinfoPhone{ + PhoneNumber: j.Phone, + PhoneNumberVerified: j.PhoneVerified, + } +} + +func (j *jsonToken) UnmarshalUserinfoAddress() *UserinfoAddress { + if j.JsonUserinfoAddress == nil { + return nil + } + return &UserinfoAddress{ + Country: j.JsonUserinfoAddress.Country, + Formatted: j.JsonUserinfoAddress.Formatted, + Locality: j.JsonUserinfoAddress.Locality, + PostalCode: j.JsonUserinfoAddress.PostalCode, + Region: j.JsonUserinfoAddress.Region, + StreetAddress: j.JsonUserinfoAddress.StreetAddress, + } +} + func ClaimHash(claim string, sigAlgorithm jose.SignatureAlgorithm) (string, error) { hash, err := utils.GetHashAlgorithm(sigAlgorithm) if err != nil { diff --git a/pkg/oidc/userinfo.go b/pkg/oidc/userinfo.go index 5e99d09..c25f51b 100644 --- a/pkg/oidc/userinfo.go +++ b/pkg/oidc/userinfo.go @@ -9,18 +9,14 @@ import ( type Userinfo struct { Subject string - Address *UserinfoAddress UserinfoProfile UserinfoEmail UserinfoPhone + Address *UserinfoAddress claims map[string]interface{} } -type UserinfoPhone struct { - PhoneNumber string - PhoneNumberVerified bool -} type UserinfoProfile struct { Name string GivenName string @@ -40,6 +36,16 @@ type UserinfoProfile struct { type Gender string +type UserinfoEmail struct { + Email string + EmailVerified bool +} + +type UserinfoPhone struct { + PhoneNumber string + PhoneNumberVerified bool +} + type UserinfoAddress struct { Formatted string StreetAddress string @@ -49,67 +55,47 @@ type UserinfoAddress struct { Country string } -type UserinfoEmail struct { - Email string - EmailVerified bool +type jsonUserinfoProfile struct { + Name string `json:"name,omitempty"` + GivenName string `json:"given_name,omitempty"` + FamilyName string `json:"family_name,omitempty"` + MiddleName string `json:"middle_name,omitempty"` + Nickname string `json:"nickname,omitempty"` + Profile string `json:"profile,omitempty"` + Picture string `json:"picture,omitempty"` + Website string `json:"website,omitempty"` + Gender string `json:"gender,omitempty"` + Birthdate string `json:"birthdate,omitempty"` + Zoneinfo string `json:"zoneinfo,omitempty"` + Locale string `json:"locale,omitempty"` + UpdatedAt int64 `json:"updated_at,omitempty"` + PreferredUsername string `json:"preferred_username,omitempty"` } -func marshalUserinfoProfile(i UserinfoProfile, claims map[string]interface{}) { - claims["name"] = i.Name - claims["given_name"] = i.GivenName - claims["family_name"] = i.FamilyName - claims["middle_name"] = i.MiddleName - claims["nickname"] = i.Nickname - claims["profile"] = i.Profile - claims["picture"] = i.Picture - claims["website"] = i.Website - claims["gender"] = i.Gender - claims["birthdate"] = i.Birthdate - claims["Zoneinfo"] = i.Zoneinfo - claims["locale"] = i.Locale.String() - claims["updated_at"] = i.UpdatedAt.UTC().Unix() - claims["preferred_username"] = i.PreferredUsername +type jsonUserinfoEmail struct { + Email string `json:"email,omitempty"` + EmailVerified bool `json:"email_verified,omitempty"` } -func marshalUserinfoEmail(i UserinfoEmail, claims map[string]interface{}) { - if i.Email != "" { - claims["email"] = i.Email - } - if i.EmailVerified { - claims["email_verified"] = i.EmailVerified - } +type jsonUserinfoPhone struct { + Phone string `json:"phone_number,omitempty"` + PhoneVerified bool `json:"phone_number_verified,omitempty"` } -func marshalUserinfoAddress(i *UserinfoAddress, claims map[string]interface{}) { - if i == nil { - return - } - address := make(map[string]interface{}) - if i.Formatted != "" { - address["formatted"] = i.Formatted - } - if i.StreetAddress != "" { - address["street_address"] = i.StreetAddress - } - claims["address"] = address -} - -func marshalUserinfoPhone(i UserinfoPhone, claims map[string]interface{}) { - claims["phone_number"] = i.PhoneNumber - claims["phone_number_verified"] = i.PhoneNumberVerified +type jsonUserinfoAddress struct { + Formatted string `json:"formatted,omitempty"` + StreetAddress string `json:"street_address,omitempty"` + Locality string `json:"locality,omitempty"` + Region string `json:"region,omitempty"` + PostalCode string `json:"postal_code,omitempty"` + Country string `json:"country,omitempty"` } func (i *Userinfo) MarshalJSON() ([]byte, error) { - claims := i.claims - if claims == nil { - claims = make(map[string]interface{}) - } - claims["sub"] = i.Subject - marshalUserinfoAddress(i.Address, claims) - marshalUserinfoEmail(i.UserinfoEmail, claims) - marshalUserinfoPhone(i.UserinfoPhone, claims) - marshalUserinfoProfile(i.UserinfoProfile, claims) - return json.Marshal(claims) + j := new(jsonUserinfo) + j.Subject = i.Subject + j.setUserinfo(*i) + return json.Marshal(j) } func (i *Userinfo) UnmmarshalJSON(data []byte) error { @@ -118,3 +104,63 @@ func (i *Userinfo) UnmmarshalJSON(data []byte) error { } return json.Unmarshal(data, i.claims) } + +type jsonUserinfo struct { + Subject string `json:"sub,omitempty"` + jsonUserinfoProfile + jsonUserinfoEmail + jsonUserinfoPhone + JsonUserinfoAddress *jsonUserinfoAddress `json:"address,omitempty"` +} + +func (j *jsonUserinfo) setUserinfo(i Userinfo) { + j.setUserinfoProfile(i.UserinfoProfile) + j.setUserinfoEmail(i.UserinfoEmail) + j.setUserinfoPhone(i.UserinfoPhone) + j.setUserinfoAddress(i.Address) +} + +func (j *jsonUserinfo) setUserinfoProfile(i UserinfoProfile) { + j.Name = i.Name + j.GivenName = i.GivenName + j.FamilyName = i.FamilyName + j.MiddleName = i.MiddleName + j.Nickname = i.Nickname + j.Profile = i.Profile + j.Picture = i.Picture + j.Website = i.Website + j.Gender = string(i.Gender) + j.Birthdate = i.Birthdate + j.Zoneinfo = i.Zoneinfo + if i.Locale != language.Und { + j.Locale = i.Locale.String() + } + j.UpdatedAt = timeToJSON(i.UpdatedAt) + j.PreferredUsername = i.PreferredUsername +} + +func (j *jsonUserinfo) setUserinfoEmail(i UserinfoEmail) { + j.Email = i.Email + j.EmailVerified = i.EmailVerified +} + +func (j *jsonUserinfo) setUserinfoPhone(i UserinfoPhone) { + j.Phone = i.PhoneNumber + j.PhoneVerified = i.PhoneNumberVerified +} + +func (j *jsonUserinfo) setUserinfoAddress(i *UserinfoAddress) { + if i == nil { + return + } + j.JsonUserinfoAddress.Country = i.Country + j.JsonUserinfoAddress.Formatted = i.Formatted + j.JsonUserinfoAddress.Locality = i.Locality + j.JsonUserinfoAddress.PostalCode = i.PostalCode + j.JsonUserinfoAddress.Region = i.Region + j.JsonUserinfoAddress.StreetAddress = i.StreetAddress +} + +type UserInfoRequest struct { + AccessToken string `schema:"access_token"` +} diff --git a/pkg/op/mock/storage.mock.go b/pkg/op/mock/storage.mock.go index ac06842..04316c3 100644 --- a/pkg/op/mock/storage.mock.go +++ b/pkg/op/mock/storage.mock.go @@ -154,18 +154,33 @@ func (mr *MockStorageMockRecorder) GetSigningKey(arg0, arg1, arg2, arg3 interfac } // GetUserinfoFromScopes mocks base method -func (m *MockStorage) GetUserinfoFromScopes(arg0 context.Context, arg1 []string) (*oidc.Userinfo, error) { +func (m *MockStorage) GetUserinfoFromScopes(arg0 context.Context, arg1 string, arg2 []string) (*oidc.Userinfo, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserinfoFromScopes", arg0, arg1) + ret := m.ctrl.Call(m, "GetUserinfoFromScopes", arg0, arg1, arg2) 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 interface{}) *gomock.Call { +func (mr *MockStorageMockRecorder) GetUserinfoFromScopes(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserinfoFromScopes", reflect.TypeOf((*MockStorage)(nil).GetUserinfoFromScopes), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserinfoFromScopes", reflect.TypeOf((*MockStorage)(nil).GetUserinfoFromScopes), arg0, arg1, arg2) +} + +// GetUserinfoFromToken mocks base method +func (m *MockStorage) GetUserinfoFromToken(arg0 context.Context, arg1 string) (*oidc.Userinfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserinfoFromToken", arg0, arg1) + 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 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserinfoFromToken", reflect.TypeOf((*MockStorage)(nil).GetUserinfoFromToken), arg0, arg1) } // Health mocks base method diff --git a/pkg/op/storage.go b/pkg/op/storage.go index b770360..f213618 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -26,7 +26,8 @@ type AuthStorage interface { type OPStorage interface { GetClientByClientID(context.Context, string) (Client, error) AuthorizeClientIDSecret(context.Context, string, string) error - GetUserinfoFromScopes(context.Context, []string) (*oidc.Userinfo, error) + GetUserinfoFromScopes(context.Context, string, []string) (*oidc.Userinfo, error) + GetUserinfoFromToken(context.Context, string) (*oidc.Userinfo, error) } type Storage interface { diff --git a/pkg/op/token.go b/pkg/op/token.go index ff74d69..06e9f9c 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -24,7 +24,7 @@ func CreateTokenResponse(ctx context.Context, authReq AuthRequest, client Client return nil, err } } - idToken, err := CreateIDToken(creator.Issuer(), authReq, client.IDTokenLifetime(), accessToken, code, creator.Signer()) + idToken, err := CreateIDToken(ctx, creator.Issuer(), authReq, client.IDTokenLifetime(), accessToken, code, creator.Storage(), creator.Signer()) if err != nil { return nil, err } @@ -71,12 +71,15 @@ func CreateJWT(issuer string, authReq AuthRequest, exp time.Time, id string, sig return signer.SignAccessToken(claims) } -func CreateIDToken(issuer string, authReq AuthRequest, validity time.Duration, accessToken, code string, signer Signer) (string, error) { +func CreateIDToken(ctx context.Context, issuer string, authReq AuthRequest, validity time.Duration, accessToken, code string, storage Storage, signer Signer) (string, error) { var err error exp := time.Now().UTC().Add(validity) + userinfo, err := storage.GetUserinfoFromScopes(ctx, authReq.GetSubject(), authReq.GetScopes()) + if err != nil { + + } claims := &oidc.IDTokenClaims{ Issuer: issuer, - Subject: authReq.GetSubject(), Audiences: authReq.GetAudience(), Expiration: exp, IssuedAt: time.Now().UTC(), @@ -85,6 +88,7 @@ func CreateIDToken(issuer string, authReq AuthRequest, validity time.Duration, a AuthenticationContextClassReference: authReq.GetACR(), AuthenticationMethodsReferences: authReq.GetAMR(), AuthorizedParty: authReq.GetClientID(), + Userinfo: *userinfo, } if accessToken != "" { claims.AccessTokenHash, err = oidc.ClaimHash(accessToken, signer.SignatureAlgorithm()) diff --git a/pkg/op/userinfo.go b/pkg/op/userinfo.go index ac47e68..69746c7 100644 --- a/pkg/op/userinfo.go +++ b/pkg/op/userinfo.go @@ -1,21 +1,33 @@ package op import ( + "errors" "net/http" + "strings" + "github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/utils" + "github.com/gorilla/schema" ) type UserinfoProvider interface { + Decoder() *schema.Decoder + Crypto() Crypto Storage() Storage } func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoProvider) { - scopes, err := ScopesFromAccessToken(w, r) + accessToken, err := getAccessToken(r, userinfoProvider.Decoder()) if err != nil { + http.Error(w, "access token missing", http.StatusUnauthorized) return } - info, err := userinfoProvider.Storage().GetUserinfoFromScopes(r.Context(), scopes) + tokenID, err := userinfoProvider.Crypto().Decrypt(accessToken) + if err != nil { + http.Error(w, "access token missing", http.StatusUnauthorized) + return + } + info, err := userinfoProvider.Storage().GetUserinfoFromToken(r.Context(), tokenID) if err != nil { utils.MarshalJSON(w, err) return @@ -23,6 +35,23 @@ func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoP utils.MarshalJSON(w, info) } -func ScopesFromAccessToken(w http.ResponseWriter, r *http.Request) ([]string, error) { - return []string{}, nil +func getAccessToken(r *http.Request, decoder *schema.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 + } + err := r.ParseForm() + if err != nil { + return "", errors.New("unable to parse request") + } + req := new(oidc.UserInfoRequest) + err = decoder.Decode(req, r.Form) + if err != nil { + return "", errors.New("unable to parse request") + } + return req.AccessToken, nil }