Fix: userinfo (#15)

* add idea to gitignore

* working userinfo

* cleanup

* tests
This commit is contained in:
livio-a 2020-03-06 17:14:30 +01:00 committed by GitHub
parent 5af734d72f
commit 2b9f7dfd18
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 225 additions and 73 deletions

1
.gitignore vendored
View file

@ -14,3 +14,4 @@
**/__debug_bin **/__debug_bin
.vscode .vscode
.DS_Store .DS_Store
.idea

View file

@ -191,7 +191,10 @@ func (s *AuthStorage) AuthorizeClientIDSecret(_ context.Context, id string, _ st
return nil 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{ return &oidc.Userinfo{
Subject: a.GetSubject(), Subject: a.GetSubject(),
Address: &oidc.UserinfoAddress{ Address: &oidc.UserinfoAddress{

View file

@ -7,6 +7,7 @@ import (
"github.com/caos/oidc/pkg/utils" "github.com/caos/oidc/pkg/utils"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/text/language"
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
) )
@ -38,7 +39,6 @@ type AccessTokenClaims struct {
type IDTokenClaims struct { type IDTokenClaims struct {
Issuer string Issuer string
Subject string
Audiences []string Audiences []string
Expiration time.Time Expiration time.Time
NotBefore time.Time NotBefore time.Time
@ -53,6 +53,7 @@ type IDTokenClaims struct {
AuthenticationContextClassReference string AuthenticationContextClassReference string
AuthenticationMethodsReferences []string AuthenticationMethodsReferences []string
ClientID string ClientID string
Userinfo
Signature jose.SignatureAlgorithm //TODO: ??? Signature jose.SignatureAlgorithm //TODO: ???
} }
@ -65,7 +66,6 @@ type jsonToken struct {
NotBefore int64 `json:"nbf,omitempty"` NotBefore int64 `json:"nbf,omitempty"`
IssuedAt int64 `json:"iat,omitempty"` IssuedAt int64 `json:"iat,omitempty"`
JWTID string `json:"jti,omitempty"` JWTID string `json:"jti,omitempty"`
UpdatedAt int64 `json:"updated_at,omitempty"`
AuthorizedParty string `json:"azp,omitempty"` AuthorizedParty string `json:"azp,omitempty"`
Nonce string `json:"nonce,omitempty"` Nonce string `json:"nonce,omitempty"`
AuthTime int64 `json:"auth_time,omitempty"` AuthTime int64 `json:"auth_time,omitempty"`
@ -79,6 +79,7 @@ type jsonToken struct {
ClientID string `json:"client_id,omitempty"` ClientID string `json:"client_id,omitempty"`
AuthorizedActor interface{} `json:"may_act,omitempty"` //TODO: impl AuthorizedActor interface{} `json:"may_act,omitempty"` //TODO: impl
AccessTokenUseNumber int `json:"at_use_nbr,omitempty"` AccessTokenUseNumber int `json:"at_use_nbr,omitempty"`
jsonUserinfo
} }
func (t *AccessTokenClaims) MarshalJSON() ([]byte, error) { func (t *AccessTokenClaims) MarshalJSON() ([]byte, error) {
@ -142,7 +143,6 @@ func (t *IDTokenClaims) MarshalJSON() ([]byte, error) {
NotBefore: timeToJSON(t.NotBefore), NotBefore: timeToJSON(t.NotBefore),
IssuedAt: timeToJSON(t.IssuedAt), IssuedAt: timeToJSON(t.IssuedAt),
JWTID: t.JWTID, JWTID: t.JWTID,
UpdatedAt: timeToJSON(t.UpdatedAt),
AuthorizedParty: t.AuthorizedParty, AuthorizedParty: t.AuthorizedParty,
Nonce: t.Nonce, Nonce: t.Nonce,
AuthTime: timeToJSON(t.AuthTime), AuthTime: timeToJSON(t.AuthTime),
@ -152,6 +152,7 @@ func (t *IDTokenClaims) MarshalJSON() ([]byte, error) {
AuthenticationMethodsReferences: t.AuthenticationMethodsReferences, AuthenticationMethodsReferences: t.AuthenticationMethodsReferences,
ClientID: t.ClientID, ClientID: t.ClientID,
} }
j.setUserinfo(t.Userinfo)
return json.Marshal(j) return json.Marshal(j)
} }
@ -176,9 +177,61 @@ func (t *IDTokenClaims) UnmarshalJSON(b []byte) error {
t.AuthorizedParty = i.AuthorizedParty t.AuthorizedParty = i.AuthorizedParty
t.AccessTokenHash = i.AccessTokenHash t.AccessTokenHash = i.AccessTokenHash
t.CodeHash = i.CodeHash t.CodeHash = i.CodeHash
t.UserinfoProfile = i.UnmarshalUserinfoProfile()
t.UserinfoEmail = i.UnmarshalUserinfoEmail()
t.UserinfoPhone = i.UnmarshalUserinfoPhone()
t.Address = i.UnmarshalUserinfoAddress()
return nil 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) { func ClaimHash(claim string, sigAlgorithm jose.SignatureAlgorithm) (string, error) {
hash, err := utils.GetHashAlgorithm(sigAlgorithm) hash, err := utils.GetHashAlgorithm(sigAlgorithm)
if err != nil { if err != nil {

View file

@ -9,18 +9,14 @@ import (
type Userinfo struct { type Userinfo struct {
Subject string Subject string
Address *UserinfoAddress
UserinfoProfile UserinfoProfile
UserinfoEmail UserinfoEmail
UserinfoPhone UserinfoPhone
Address *UserinfoAddress
claims map[string]interface{} claims map[string]interface{}
} }
type UserinfoPhone struct {
PhoneNumber string
PhoneNumberVerified bool
}
type UserinfoProfile struct { type UserinfoProfile struct {
Name string Name string
GivenName string GivenName string
@ -40,6 +36,16 @@ type UserinfoProfile struct {
type Gender string type Gender string
type UserinfoEmail struct {
Email string
EmailVerified bool
}
type UserinfoPhone struct {
PhoneNumber string
PhoneNumberVerified bool
}
type UserinfoAddress struct { type UserinfoAddress struct {
Formatted string Formatted string
StreetAddress string StreetAddress string
@ -49,67 +55,47 @@ type UserinfoAddress struct {
Country string Country string
} }
type UserinfoEmail struct { type jsonUserinfoProfile struct {
Email string Name string `json:"name,omitempty"`
EmailVerified bool 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{}) { type jsonUserinfoEmail struct {
claims["name"] = i.Name Email string `json:"email,omitempty"`
claims["given_name"] = i.GivenName EmailVerified bool `json:"email_verified,omitempty"`
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
} }
func marshalUserinfoEmail(i UserinfoEmail, claims map[string]interface{}) { type jsonUserinfoPhone struct {
if i.Email != "" { Phone string `json:"phone_number,omitempty"`
claims["email"] = i.Email PhoneVerified bool `json:"phone_number_verified,omitempty"`
}
if i.EmailVerified {
claims["email_verified"] = i.EmailVerified
}
} }
func marshalUserinfoAddress(i *UserinfoAddress, claims map[string]interface{}) { type jsonUserinfoAddress struct {
if i == nil { Formatted string `json:"formatted,omitempty"`
return StreetAddress string `json:"street_address,omitempty"`
} Locality string `json:"locality,omitempty"`
address := make(map[string]interface{}) Region string `json:"region,omitempty"`
if i.Formatted != "" { PostalCode string `json:"postal_code,omitempty"`
address["formatted"] = i.Formatted Country string `json:"country,omitempty"`
}
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
} }
func (i *Userinfo) MarshalJSON() ([]byte, error) { func (i *Userinfo) MarshalJSON() ([]byte, error) {
claims := i.claims j := new(jsonUserinfo)
if claims == nil { j.Subject = i.Subject
claims = make(map[string]interface{}) j.setUserinfo(*i)
} return json.Marshal(j)
claims["sub"] = i.Subject
marshalUserinfoAddress(i.Address, claims)
marshalUserinfoEmail(i.UserinfoEmail, claims)
marshalUserinfoPhone(i.UserinfoPhone, claims)
marshalUserinfoProfile(i.UserinfoProfile, claims)
return json.Marshal(claims)
} }
func (i *Userinfo) UnmmarshalJSON(data []byte) error { func (i *Userinfo) UnmmarshalJSON(data []byte) error {
@ -118,3 +104,63 @@ func (i *Userinfo) UnmmarshalJSON(data []byte) error {
} }
return json.Unmarshal(data, i.claims) 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"`
}

View file

@ -154,18 +154,33 @@ func (mr *MockStorageMockRecorder) GetSigningKey(arg0, arg1, arg2, arg3 interfac
} }
// GetUserinfoFromScopes mocks base method // 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() 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) ret0, _ := ret[0].(*oidc.Userinfo)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// GetUserinfoFromScopes indicates an expected call of GetUserinfoFromScopes // 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() 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 // Health mocks base method

View file

@ -26,7 +26,8 @@ type AuthStorage interface {
type OPStorage interface { type OPStorage interface {
GetClientByClientID(context.Context, string) (Client, error) GetClientByClientID(context.Context, string) (Client, error)
AuthorizeClientIDSecret(context.Context, string, string) 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 { type Storage interface {

View file

@ -24,7 +24,7 @@ func CreateTokenResponse(ctx context.Context, authReq AuthRequest, client Client
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }
@ -71,12 +71,15 @@ func CreateJWT(issuer string, authReq AuthRequest, exp time.Time, id string, sig
return signer.SignAccessToken(claims) 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 var err error
exp := time.Now().UTC().Add(validity) exp := time.Now().UTC().Add(validity)
userinfo, err := storage.GetUserinfoFromScopes(ctx, authReq.GetSubject(), authReq.GetScopes())
if err != nil {
}
claims := &oidc.IDTokenClaims{ claims := &oidc.IDTokenClaims{
Issuer: issuer, Issuer: issuer,
Subject: authReq.GetSubject(),
Audiences: authReq.GetAudience(), Audiences: authReq.GetAudience(),
Expiration: exp, Expiration: exp,
IssuedAt: time.Now().UTC(), IssuedAt: time.Now().UTC(),
@ -85,6 +88,7 @@ func CreateIDToken(issuer string, authReq AuthRequest, validity time.Duration, a
AuthenticationContextClassReference: authReq.GetACR(), AuthenticationContextClassReference: authReq.GetACR(),
AuthenticationMethodsReferences: authReq.GetAMR(), AuthenticationMethodsReferences: authReq.GetAMR(),
AuthorizedParty: authReq.GetClientID(), AuthorizedParty: authReq.GetClientID(),
Userinfo: *userinfo,
} }
if accessToken != "" { if accessToken != "" {
claims.AccessTokenHash, err = oidc.ClaimHash(accessToken, signer.SignatureAlgorithm()) claims.AccessTokenHash, err = oidc.ClaimHash(accessToken, signer.SignatureAlgorithm())

View file

@ -1,21 +1,33 @@
package op package op
import ( import (
"errors"
"net/http" "net/http"
"strings"
"github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/utils" "github.com/caos/oidc/pkg/utils"
"github.com/gorilla/schema"
) )
type UserinfoProvider interface { type UserinfoProvider interface {
Decoder() *schema.Decoder
Crypto() Crypto
Storage() Storage Storage() Storage
} }
func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoProvider) { func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoProvider) {
scopes, err := ScopesFromAccessToken(w, r) accessToken, err := getAccessToken(r, userinfoProvider.Decoder())
if err != nil { if err != nil {
http.Error(w, "access token missing", http.StatusUnauthorized)
return 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 { if err != nil {
utils.MarshalJSON(w, err) utils.MarshalJSON(w, err)
return return
@ -23,6 +35,23 @@ func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoP
utils.MarshalJSON(w, info) utils.MarshalJSON(w, info)
} }
func ScopesFromAccessToken(w http.ResponseWriter, r *http.Request) ([]string, error) { func getAccessToken(r *http.Request, decoder *schema.Decoder) (string, error) {
return []string{}, nil 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
} }