introspect

This commit is contained in:
Livio Amstutz 2021-01-08 15:01:23 +01:00
parent d693f6113d
commit a1a21f0d59
6 changed files with 341 additions and 6 deletions

256
pkg/oidc/introspection.go Normal file
View file

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

View file

@ -13,6 +13,7 @@ type Configuration interface {
Issuer() string
AuthorizationEndpoint() Endpoint
TokenEndpoint() Endpoint
IntrospectionEndpoint() Endpoint
UserinfoEndpoint() Endpoint
EndSessionEndpoint() Endpoint
KeysEndpoint() Endpoint

View file

@ -22,7 +22,7 @@ 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()),
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),

View file

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

View file

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

View file

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