Skip to content

Commit

Permalink
feat: Allow extra fields in introspect response
Browse files Browse the repository at this point in the history
Fixes #441.
  • Loading branch information
mitar committed Oct 28, 2020
1 parent 1fcb789 commit 3cefbfb
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 25 deletions.
11 changes: 11 additions & 0 deletions handler/oauth2/strategy_jwt_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,14 @@ func (s *JWTSession) Clone() fosite.Session {

return deepcopy.Copy(s).(fosite.Session)
}

// GetExtraClaims implements ExtraClaimsSession for JWTSession.
// The returned value is a copy of JWTSession claims.
func (s *JWTSession) GetExtraClaims() map[string]interface{} {
if s == nil {
return nil
}

// We make a clone so that WithScopeField does not change the original value.
return s.Clone().(*JWTSession).GetJWTClaims().WithScopeField(jwt.JWTScopeFieldString).ToMapClaims()
}
62 changes: 37 additions & 25 deletions introspection_response_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ package fosite
import (
"encoding/json"
"net/http"
"reflect"
"strings"

"github.com/pkg/errors"
Expand Down Expand Up @@ -204,35 +205,46 @@ func (f *Fosite) WriteIntrospectionResponse(rw http.ResponseWriter, r Introspect
return
}

expiresAt := int64(0)
response := map[string]interface{}{
"active": true,
}

extraClaimsSession, ok := r.GetAccessRequester().GetSession().(ExtraClaimsSession)
if ok {
extraClaims := extraClaimsSession.GetExtraClaims()
if extraClaims != nil {
for name, value := range extraClaims {
if !reflect.ValueOf(value).IsZero() {
response[name] = value
}
}
}
}

if !r.GetAccessRequester().GetSession().GetExpiresAt(AccessToken).IsZero() {
expiresAt = r.GetAccessRequester().GetSession().GetExpiresAt(AccessToken).Unix()
response["exp"] = r.GetAccessRequester().GetSession().GetExpiresAt(AccessToken).Unix()
}
if r.GetAccessRequester().GetClient().GetID() != "" {
response["client_id"] = r.GetAccessRequester().GetClient().GetID()
}
if len(r.GetAccessRequester().GetGrantedScopes()) > 0 {
response["scope"] = strings.Join(r.GetAccessRequester().GetGrantedScopes(), " ")
}
if !r.GetAccessRequester().GetRequestedAt().IsZero() {
response["iat"] = r.GetAccessRequester().GetRequestedAt().Unix()
}
if r.GetAccessRequester().GetSession().GetSubject() != "" {
response["sub"] = r.GetAccessRequester().GetSession().GetSubject()
}
if len(r.GetAccessRequester().GetGrantedAudience()) > 0 {
response["aud"] = r.GetAccessRequester().GetGrantedAudience()
}
if r.GetAccessRequester().GetSession().GetUsername() != "" {
response["username"] = r.GetAccessRequester().GetSession().GetUsername()
}

rw.Header().Set("Content-Type", "application/json;charset=UTF-8")
rw.Header().Set("Cache-Control", "no-store")
rw.Header().Set("Pragma", "no-cache")
_ = json.NewEncoder(rw).Encode(struct {
Active bool `json:"active"`
ClientID string `json:"client_id,omitempty"`
Scope string `json:"scope,omitempty"`
Audience []string `json:"aud,omitempty"`
ExpiresAt int64 `json:"exp,omitempty"`
IssuedAt int64 `json:"iat,omitempty"`
Subject string `json:"sub,omitempty"`
Username string `json:"username,omitempty"`
// Session is not included per default because it might expose sensitive information.
// Session Session `json:"sess,omitempty"`
}{
Active: true,
ClientID: r.GetAccessRequester().GetClient().GetID(),
Scope: strings.Join(r.GetAccessRequester().GetGrantedScopes(), " "),
ExpiresAt: expiresAt,
IssuedAt: r.GetAccessRequester().GetRequestedAt().Unix(),
Subject: r.GetAccessRequester().GetSession().GetSubject(),
Audience: r.GetAccessRequester().GetGrantedAudience(),
Username: r.GetAccessRequester().GetSession().GetUsername(),
// Session is not included because it might expose sensitive information.
// Session: r.GetAccessRequester().GetSession(),
})
_ = json.NewEncoder(rw).Encode(response)
}
24 changes: 24 additions & 0 deletions introspection_response_writer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ func TestWriteIntrospectionResponseBody(t *testing.T) {
setup func()
active bool
hasExp bool
hasExtra bool
}{
{
description: "should success for not expired access token",
Expand All @@ -95,6 +96,7 @@ func TestWriteIntrospectionResponseBody(t *testing.T) {
},
active: true,
hasExp: true,
hasExtra: false,
},
{
description: "should success for expired access token",
Expand All @@ -107,6 +109,7 @@ func TestWriteIntrospectionResponseBody(t *testing.T) {
},
active: false,
hasExp: false,
hasExtra: false,
},
{
description: "should success for ExpiresAt not set access token",
Expand All @@ -119,6 +122,21 @@ func TestWriteIntrospectionResponseBody(t *testing.T) {
},
active: true,
hasExp: false,
hasExtra: false,
},
{
description: "should output extra claims",
setup: func() {
ires.Active = true
ires.TokenUse = AccessToken
sess := &DefaultSession{}
sess.GetExtraClaims()["extra"] = "foobar"
sess.SetExpiresAt(ires.TokenUse, time.Time{})
ires.AccessRequester = NewAccessRequest(sess)
},
active: true,
hasExp: false,
hasExtra: true,
},
} {
t.Run(c.description, func(t *testing.T) {
Expand All @@ -128,6 +146,7 @@ func TestWriteIntrospectionResponseBody(t *testing.T) {
Active bool `json:"active"`
Exp *int64 `json:"exp"`
Iat *int64 `json:"iat"`
Extra string `json:"extra"`
}
assert.Equal(t, 200, rw.Code)
err := json.NewDecoder(rw.Body).Decode(&params)
Expand All @@ -140,6 +159,11 @@ func TestWriteIntrospectionResponseBody(t *testing.T) {
} else {
assert.Nil(t, params.Exp)
}
if c.hasExtra {
assert.Equal(t, params.Extra, "foobar")
} else {
assert.Empty(t, params.Extra)
}
}
})
}
Expand Down
22 changes: 22 additions & 0 deletions session.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ type DefaultSession struct {
ExpiresAt map[TokenType]time.Time
Username string
Subject string
Extra map[string]interface{}
}

func (s *DefaultSession) SetExpiresAt(key TokenType, exp time.Time) {
Expand Down Expand Up @@ -97,3 +98,24 @@ func (s *DefaultSession) Clone() Session {

return deepcopy.Copy(s).(Session)
}

// ExtraClaimsSession provides an interface for session to store any extra claims.
type ExtraClaimsSession interface {
// GetExtraClaims returns a map to store extra claims.
// The returned value can be modified in-place.
GetExtraClaims() map[string]interface{}
}

// GetExtraClaims implements ExtraClaimsSession for DefaultSession.
// The returned value can be modified in-place.
func (s *DefaultSession) GetExtraClaims() map[string]interface{} {
if s == nil {
return nil
}

if s.Extra == nil {
s.Extra = make(map[string]interface{})
}

return s.Extra
}

0 comments on commit 3cefbfb

Please sign in to comment.