diff --git a/pkg/oidc/introspection.go b/pkg/oidc/introspection.go new file mode 100644 index 0000000..7fa13de --- /dev/null +++ b/pkg/oidc/introspection.go @@ -0,0 +1,256 @@ +package oidc + +import ( + "encoding/json" + "fmt" + "time" + + "golang.org/x/text/language" + + "github.com/caos/oidc/pkg/utils" +) + +type IntrospectionRequest struct { + Token string `schema:"token"` +} + +type IntrospectionResponse interface { + UserInfoSetter + SetActive(bool) + IsActive() bool +} + +func NewIntrospectionResponse() IntrospectionResponse { + return &introspectionResponse{} +} + +type introspectionResponse struct { + Active bool `json:"active"` + Subject string `json:"sub,omitempty"` + userInfoProfile + userInfoEmail + userInfoPhone + Address UserInfoAddress `json:"address,omitempty"` + + claims map[string]interface{} +} + +func (u *introspectionResponse) IsActive() bool { + return u.Active +} +func (u *introspectionResponse) GetSubject() string { + return u.Subject +} + +func (u *introspectionResponse) GetName() string { + return u.Name +} + +func (u *introspectionResponse) GetGivenName() string { + return u.GivenName +} + +func (u *introspectionResponse) GetFamilyName() string { + return u.FamilyName +} + +func (u *introspectionResponse) GetMiddleName() string { + return u.MiddleName +} + +func (u *introspectionResponse) GetNickname() string { + return u.Nickname +} + +func (u *introspectionResponse) GetProfile() string { + return u.Profile +} + +func (u *introspectionResponse) GetPicture() string { + return u.Picture +} + +func (u *introspectionResponse) GetWebsite() string { + return u.Website +} + +func (u *introspectionResponse) GetGender() Gender { + return u.Gender +} + +func (u *introspectionResponse) GetBirthdate() string { + return u.Birthdate +} + +func (u *introspectionResponse) GetZoneinfo() string { + return u.Zoneinfo +} + +func (u *introspectionResponse) GetLocale() language.Tag { + return u.Locale +} + +func (u *introspectionResponse) GetPreferredUsername() string { + return u.PreferredUsername +} + +func (u *introspectionResponse) GetEmail() string { + return u.Email +} + +func (u *introspectionResponse) IsEmailVerified() bool { + return u.EmailVerified +} + +func (u *introspectionResponse) GetPhoneNumber() string { + return u.PhoneNumber +} + +func (u *introspectionResponse) IsPhoneNumberVerified() bool { + return u.PhoneNumberVerified +} + +func (u *introspectionResponse) GetAddress() UserInfoAddress { + return u.Address +} + +func (u *introspectionResponse) GetClaim(key string) interface{} { + return u.claims[key] +} + +func (u *introspectionResponse) SetActive(active bool) { + u.Active = active +} + +func (u *introspectionResponse) SetSubject(sub string) { + u.Subject = sub +} + +func (u *introspectionResponse) SetName(name string) { + u.Name = name +} + +func (u *introspectionResponse) SetGivenName(name string) { + u.GivenName = name +} + +func (u *introspectionResponse) SetFamilyName(name string) { + u.FamilyName = name +} + +func (u *introspectionResponse) SetMiddleName(name string) { + u.MiddleName = name +} + +func (u *introspectionResponse) SetNickname(name string) { + u.Nickname = name +} + +func (u *introspectionResponse) SetUpdatedAt(date time.Time) { + u.UpdatedAt = Time(date) +} + +func (u *introspectionResponse) SetProfile(profile string) { + u.Profile = profile +} + +func (u *introspectionResponse) SetPicture(picture string) { + u.Picture = picture +} + +func (u *introspectionResponse) SetWebsite(website string) { + u.Website = website +} + +func (u *introspectionResponse) SetGender(gender Gender) { + u.Gender = gender +} + +func (u *introspectionResponse) SetBirthdate(birthdate string) { + u.Birthdate = birthdate +} + +func (u *introspectionResponse) SetZoneinfo(zoneInfo string) { + u.Zoneinfo = zoneInfo +} + +func (u *introspectionResponse) SetLocale(locale language.Tag) { + u.Locale = locale +} + +func (u *introspectionResponse) SetPreferredUsername(name string) { + u.PreferredUsername = name +} + +func (u *introspectionResponse) SetEmail(email string, verified bool) { + u.Email = email + u.EmailVerified = verified +} + +func (u *introspectionResponse) SetPhone(phone string, verified bool) { + u.PhoneNumber = phone + u.PhoneNumberVerified = verified +} + +func (u *introspectionResponse) SetAddress(address UserInfoAddress) { + u.Address = address +} + +func (u *introspectionResponse) AppendClaims(key string, value interface{}) { + if u.claims == nil { + u.claims = make(map[string]interface{}) + } + u.claims[key] = value +} + +func (i *introspectionResponse) MarshalJSON() ([]byte, error) { + type Alias introspectionResponse + a := &struct { + *Alias + Locale interface{} `json:"locale,omitempty"` + UpdatedAt int64 `json:"updated_at,omitempty"` + PreferredUsername string `json:"username,omitempty"` + }{ + Alias: (*Alias)(i), + } + if !i.Locale.IsRoot() { + a.Locale = i.Locale + } + if !time.Time(i.UpdatedAt).IsZero() { + a.UpdatedAt = time.Time(i.UpdatedAt).Unix() + } + a.PreferredUsername = i.PreferredUsername + i.PreferredUsername = "" + + b, err := json.Marshal(a) + if err != nil { + return nil, err + } + + if len(i.claims) == 0 { + return b, nil + } + + claims, err := json.Marshal(i.claims) + if err != nil { + return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims) + } + return utils.ConcatenateJSON(b, claims) +} + +func (i *introspectionResponse) UnmarshalJSON(data []byte) error { + type Alias introspectionResponse + a := &struct { + *Alias + UpdatedAt int64 `json:"update_at,omitempty"` + }{ + Alias: (*Alias)(i), + } + if err := json.Unmarshal(data, &a); err != nil { + return err + } + + i.UpdatedAt = Time(time.Unix(a.UpdatedAt, 0).UTC()) + + return nil +} diff --git a/pkg/op/config.go b/pkg/op/config.go index a2b831e..f4fbb97 100644 --- a/pkg/op/config.go +++ b/pkg/op/config.go @@ -13,6 +13,7 @@ type Configuration interface { Issuer() string AuthorizationEndpoint() Endpoint TokenEndpoint() Endpoint + IntrospectionEndpoint() Endpoint UserinfoEndpoint() Endpoint EndSessionEndpoint() Endpoint KeysEndpoint() Endpoint diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 4bc1272..3bec79b 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -22,9 +22,9 @@ func CreateDiscoveryConfig(c Configuration, s Signer) *oidc.DiscoveryConfigurati Issuer: c.Issuer(), AuthorizationEndpoint: c.AuthorizationEndpoint().Absolute(c.Issuer()), TokenEndpoint: c.TokenEndpoint().Absolute(c.Issuer()), - // IntrospectionEndpoint: c.Intro().Absolute(c.Issuer()), - UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()), - EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()), + IntrospectionEndpoint: c.IntrospectionEndpoint().Absolute(c.Issuer()), + UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()), + EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()), // CheckSessionIframe: c.TokenEndpoint().Absolute(c.Issuer())(c.CheckSessionIframe), JwksURI: c.KeysEndpoint().Absolute(c.Issuer()), ScopesSupported: Scopes(c), diff --git a/pkg/op/op.go b/pkg/op/op.go index d16848e..76d5fcc 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -21,7 +21,7 @@ const ( readinessEndpoint = "/ready" defaultAuthorizationEndpoint = "authorize" defaulTokenEndpoint = "oauth/token" - defaultIntrospectEndpoint = "introspect" + defaultIntrospectEndpoint = "oauth/introspect" defaultUserinfoEndpoint = "userinfo" defaultEndSessionEndpoint = "end_session" defaultKeysEndpoint = "keys" @@ -78,6 +78,7 @@ func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router router.Handle(o.AuthorizationEndpoint().Relative(), intercept(authorizeHandler(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.IntrospectionEndpoint().Relative(), introspectionHandler(o)) router.HandleFunc(o.UserinfoEndpoint().Relative(), userinfoHandler(o)) router.Handle(o.EndSessionEndpoint().Relative(), intercept(endSessionHandler(o))) router.HandleFunc(o.KeysEndpoint().Relative(), keysHandler(o)) @@ -166,6 +167,10 @@ func (o *openidProvider) TokenEndpoint() Endpoint { return o.endpoints.Token } +func (o *openidProvider) IntrospectionEndpoint() Endpoint { + return o.endpoints.Introspection +} + func (o *openidProvider) UserinfoEndpoint() Endpoint { return o.endpoints.Userinfo } @@ -332,6 +337,16 @@ func WithCustomTokenEndpoint(endpoint Endpoint) Option { } } +func WithCustomIntrospectionEndpoint(endpoint Endpoint) Option { + return func(o *openidProvider) error { + if err := endpoint.Validate(); err != nil { + return err + } + o.endpoints.Introspection = endpoint + return nil + } +} + func WithCustomUserinfoEndpoint(endpoint Endpoint) Option { return func(o *openidProvider) error { if err := endpoint.Validate(); err != nil { diff --git a/pkg/op/storage.go b/pkg/op/storage.go index eba5003..0a0cec2 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -28,10 +28,15 @@ type AuthStorage interface { type OPStorage interface { GetClientByClientID(ctx context.Context, clientID string) (Client, error) AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error - GetUserinfoFromScopes(ctx context.Context, userID, clientID string, scopes []string) (oidc.UserInfo, error) - GetUserinfoFromToken(ctx context.Context, tokenID, subject, origin string) (oidc.UserInfo, error) + SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error + SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]interface{}, error) GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) + + //deprecated: use GetUserinfoFromScopes instead + GetUserinfoFromScopes(ctx context.Context, userID, clientID string, scopes []string) (oidc.UserInfo, error) + //deprecated: use SetUserinfoFromToken instead + GetUserinfoFromToken(ctx context.Context, tokenID, subject, origin string) (oidc.UserInfo, error) } type Storage interface { diff --git a/pkg/op/token_intospection.go b/pkg/op/token_intospection.go new file mode 100644 index 0000000..e6a5d4a --- /dev/null +++ b/pkg/op/token_intospection.go @@ -0,0 +1,58 @@ +package op + +import ( + "errors" + "net/http" + + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/utils" +) + +type Introspector interface { + Decoder() utils.Decoder + Crypto() Crypto + Storage() Storage + AccessTokenVerifier() AccessTokenVerifier +} + +func introspectionHandler(introspector Introspector) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + Introspect(w, r, introspector) + } +} + +func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspector) { + //validate authorization + + response := oidc.NewIntrospectionResponse() + token, err := ParseTokenInrospectionRequest(r, introspector.Decoder()) + if err != nil { + utils.MarshalJSON(w, response) + return + } + tokenID, subject, ok := getTokenIDAndSubject(r.Context(), introspector, token) + if !ok { + utils.MarshalJSON(w, response) + return + } + err = introspector.Storage().SetUserinfoFromToken(r.Context(), response, tokenID, subject, r.Header.Get("origin")) + if err != nil { + utils.MarshalJSON(w, response) + return + } + response.SetActive(true) + utils.MarshalJSON(w, response) +} + +func ParseTokenInrospectionRequest(r *http.Request, decoder utils.Decoder) (string, error) { + err := r.ParseForm() + if err != nil { + return "", errors.New("unable to parse request") + } + req := new(oidc.IntrospectionRequest) + err = decoder.Decode(req, r.Form) + if err != nil { + return "", errors.New("unable to parse request") + } + return req.Token, nil +}