Skip to content

Commit

Permalink
feat: token introspection
Browse files Browse the repository at this point in the history
  • Loading branch information
james-d-elliott committed Dec 25, 2023
1 parent e754bd7 commit 3040c6f
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 14 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Several differences or intended differences exist between this package and the g
- Add support for:
- [ ] [JWT Secured Authorization Response Mode for OAuth 2.0](https://openid.net/specs/oauth-v2-jarm.html) (JARM) implementation.
- [ ] [OpenID Connect 1.0 Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) implementation.
- [ ] [RFC7662: OAuth 2.0 Token Introspection](https://datatracker.ietf.org/doc/html/rfc7662)
- [x] [RFC7662: OAuth 2.0 Token Introspection](https://datatracker.ietf.org/doc/html/rfc7662)
- [x] [RFC7009: OAuth 2.0 Token Revocation](https://datatracker.ietf.org/doc/html/rfc7009)
- [ ] [RFC8414: OAuth 2.0 Authorization Server Metadata](https://datatracker.ietf.org/doc/html/rfc8414)
- [x] [RFC9126: OAuth 2.0 Pushed Authorization Requests (PAR)](https://datatracker.ietf.org/doc/html/rfc9126)
Expand Down
99 changes: 93 additions & 6 deletions internal/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,51 @@ func cloneURLValues(v url.Values) url.Values {
return v2
}

type Introspection struct {
Active bool `json:"active"`
JTI string `json:"jti,omitempty"`
Issuer string `json:"iss,omitempty"`
Scope string `json:"scope,omitempty"`
Audience []string `json:"aud,omitempty"`
ClientID string `json:"client_id,omitempty"`
TokenType string `json:"token_type,omitempty"`
Expiry time.Time `json:"exp,omitempty"`
IssuedAt time.Time `json:"iat,omitempty"`
NotBefore time.Time `json:"nbf,omitempty"`
Subject string `json:"sub,omitempty"`
Error string `json:"error,omitempty"`
ErrorDescription string `json:"error_description,omitempty"`
ErrorURI string `json:"error_uri,omitempty"`
Raw map[string]any `json:"-"`
}

func IntrospectToken(ctx context.Context, clientID, clientSecret, introspectionURL string, v url.Values, authStyle AuthStyle, styleCache *AuthStyleCache) (introspection *Introspection, err error) {
needsAuthStyleProbe := authStyle == 0
if needsAuthStyleProbe {
if style, ok := styleCache.lookupAuthStyle(introspectionURL); ok {
authStyle = style
needsAuthStyleProbe = false
} else {
authStyle = AuthStyleInHeader // the first way we'll try
}
}
req, err := newPOSTRequest(introspectionURL, clientID, clientSecret, v, authStyle)
if err != nil {
return nil, err
}

if introspection, err = doIntrospectRoundTrip(ctx, req); err != nil && needsAuthStyleProbe {
authStyle = AuthStyleInParams // the second way we'll try
req, _ = newPOSTRequest(introspectionURL, clientID, clientSecret, v, authStyle)
introspection, err = doIntrospectRoundTrip(ctx, req)
}
if needsAuthStyleProbe && err == nil {
styleCache.setAuthStyle(introspectionURL, authStyle)
}

return introspection, err
}

func RevokeToken(ctx context.Context, clientID, clientSecret, revocationURL string, v url.Values, authStyle AuthStyle, styleCache *AuthStyleCache) error {
needsAuthStyleProbe := authStyle == 0
if needsAuthStyleProbe {
Expand Down Expand Up @@ -284,6 +329,48 @@ func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string,
return token, err
}

func doIntrospectRoundTrip(ctx context.Context, req *http.Request) (introspection *Introspection, err error) {
r, err := ContextClient(ctx).Do(req.WithContext(ctx))
if err != nil {
return nil, err
}
body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
r.Body.Close()
if err != nil {
return nil, fmt.Errorf("oauth2: cannot introspect token: %v", err)
}

content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))

introspectError := &BaseError{
Response: r,
Body: body,
}

switch content {
case "application/json":
resp := &Introspection{}

if err = json.Unmarshal(body, resp); err != nil {
// TODO: REMOVE.
fmt.Printf("error occurred unmarshalling object: %+v", err)

return nil, introspectError
}

if err = json.Unmarshal(body, &resp.Raw); err != nil {
// TODO: REMOVE.
fmt.Printf("error occurred unmarshalling raw: %+v", err)

return nil, introspectError
}

return resp, nil
}

return nil, introspectError
}

func doRevokeRoundTrip(ctx context.Context, req *http.Request) error {
r, err := ContextClient(ctx).Do(req.WithContext(ctx))
if err != nil {
Expand All @@ -295,13 +382,13 @@ func doRevokeRoundTrip(ctx context.Context, req *http.Request) error {
return fmt.Errorf("oauth2: cannot revoke token: %v", err)
}

failureStatus := r.StatusCode < 200 || r.StatusCode > 299
failureStatus := r.StatusCode < http.StatusOK || r.StatusCode >= http.StatusMultipleChoices

if !failureStatus {
return nil
}

revokeError := &RevokeError{
revokeError := &BaseError{
Response: r,
Body: body,
// attempt to populate error detail below
Expand Down Expand Up @@ -344,7 +431,7 @@ func doTokenRoundTrip(ctx context.Context, req *http.Request) (*Token, error) {
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
}

failureStatus := r.StatusCode < 200 || r.StatusCode > 299
failureStatus := r.StatusCode < http.StatusOK || r.StatusCode >= http.StatusMultipleChoices
retrieveError := &RetrieveError{
Response: r,
Body: body,
Expand Down Expand Up @@ -432,15 +519,15 @@ func (r *RetrieveError) Error() string {
return fmt.Sprintf("oauth2: cannot fetch token: %v\nResponse: %s", r.Response.Status, r.Body)
}

type RevokeError struct {
type BaseError struct {
Response *http.Response
Body []byte
ErrorCode string
ErrorDescription string
ErrorURI string
}

func (r *RevokeError) Error() string {
func (r *BaseError) Error() string {
if r.ErrorCode != "" {
s := fmt.Sprintf("oauth2: %q", r.ErrorCode)
if r.ErrorDescription != "" {
Expand All @@ -451,5 +538,5 @@ func (r *RevokeError) Error() string {
}
return s
}
return fmt.Sprintf("oauth2: cannot revoke token: %v\nResponse: %s", r.Response.Status, r.Body)
return fmt.Sprintf("oauth2: cannot perform flow: %v\nResponse: %s", r.Response.Status, r.Body)
}
93 changes: 93 additions & 0 deletions introspection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package oauth2

import (
"context"
"fmt"
"net/url"

"authelia.com/client/oauth2/internal"
)

type IntrospectionResult struct {
Introspection *internal.Introspection
Error error
}

// IntrospectToken allows for simple token introspection.
func (c *Config) IntrospectToken(ctx context.Context, token *Token, opts ...IntrospectionRevocationOption) (results []IntrospectionResult, err error) {
if token == nil {
return nil, fmt.Errorf("error introspecting token: no token was provided")
}

if c.Endpoint.IntrospectionURL == "" {
return nil, fmt.Errorf("error introspecting token: no introspection endpoint URL was provided")
}

tths := []string{}

for _, opt := range opts {
tths = opt.appendTokenTypeHints(tths)
}

if len(tths) == 0 {
tths = []string{"access_token"}
}

vals := make([]url.Values, len(tths))

for i, tth := range tths {
xvals := url.Values{
"token_type_hint": []string{tth},
}

switch tth {
case "access_token":
if len(token.AccessToken) == 0 {
return nil, fmt.Errorf("error introspecting token: token type hint '%s' can only be introspected for a token that has an access token", tth)
}

xvals.Set("token", token.AccessToken)
case "refresh_token":
if len(token.AccessToken) == 0 {
return nil, fmt.Errorf("error introspecting token: token type hint '%s' can only be introspected for a token that has a refresh token", tth)
}

xvals.Set("token", token.RefreshToken)
default:
return nil, fmt.Errorf("error introspecting token: token type hint '%s' isn't known", tth)
}

for _, opt := range opts {
opt.setValue(xvals)
}

vals[i] = xvals
}

var (
introspection *internal.Introspection
errored bool
)

for _, v := range vals {
if introspection, err = internal.IntrospectToken(ctx, c.ClientID, c.ClientSecret, c.Endpoint.IntrospectionURL, v, internal.AuthStyle(c.Endpoint.AuthStyle), c.authStyleCache.Get()); err != nil {
errored = true

if rErr, ok := err.(*internal.BaseError); ok {
results = append(results, IntrospectionResult{Introspection: introspection, Error: (*BaseError)(rErr)})

continue
}

results = append(results, IntrospectionResult{Introspection: introspection, Error: err})
}

results = append(results, IntrospectionResult{Introspection: introspection})
}

if errored {
return results, fmt.Errorf("error introspecting token: one or more errors occurred check the results for details")
}

return results, nil
}
15 changes: 8 additions & 7 deletions revocation.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
)

// RevokeToken allows for simple token revocation.
func (c *Config) RevokeToken(ctx context.Context, token *Token, opts ...RevocationOption) (err error) {
func (c *Config) RevokeToken(ctx context.Context, token *Token, opts ...IntrospectionRevocationOption) (err error) {
if token == nil {
return fmt.Errorf("error revoking token: no token was provided")
}
Expand Down Expand Up @@ -61,7 +61,7 @@ func (c *Config) RevokeToken(ctx context.Context, token *Token, opts ...Revocati

for _, v := range vals {
if err = internal.RevokeToken(ctx, c.ClientID, c.ClientSecret, c.Endpoint.RevocationURL, v, internal.AuthStyle(c.Endpoint.AuthStyle), c.authStyleCache.Get()); err != nil {
if rErr, ok := err.(*internal.RevokeError); ok {
if rErr, ok := err.(*internal.BaseError); ok {
xErr := (*BaseError)(rErr)

return &RevokeError{xErr}
Expand All @@ -77,23 +77,24 @@ type RevokeError struct {
*BaseError
}

type RevocationOption interface {
// IntrospectionRevocationOption is an interface that can be utilized for both introspection and revocation requests.
type IntrospectionRevocationOption interface {
setValue(vals url.Values)
appendTokenTypeHints(tths []string) []string
}

// SetRevocationURLParam builds a RevocationOption which passes key/value parameters
// SetRevocationURLParam builds a IntrospectionRevocationOption which passes key/value parameters
// to a provider's revocation endpoint.
func SetRevocationURLParam(key, value string) RevocationOption {
func SetRevocationURLParam(key, value string) IntrospectionRevocationOption {
return setRevocationValue{key, value}
}

// AddRevocationTokenTypes builds a RevocationOption which explicitly adds a token
// AddRevocationTokenTypes builds a IntrospectionRevocationOption which explicitly adds a token
// type hint to the revocation process. By default the oauth2.RevokeToken method
// will perform the access token revocation. If the authorization server requires
// the refresh token is revoked manually then use this option like
// oauth.AddRevocationTokenTypes("access_token", "refresh_token").
func AddRevocationTokenTypes(values ...string) RevocationOption {
func AddRevocationTokenTypes(values ...string) IntrospectionRevocationOption {
return addRevocationTokenTypeHints{values: values}
}

Expand Down

0 comments on commit 3040c6f

Please sign in to comment.