Skip to content

Commit add3cbe

Browse files
feat(rfc8693): add rfc8693 implementation (#46)
This is an implementation of the RFC8693 OAuth 2.0 Token Exchange flow. See https://datatracker.ietf.org/doc/html/rfc8693.
1 parent be08837 commit add3cbe

26 files changed

+1553
-22
lines changed

config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,12 @@ type RFC8628UserAuthorizeEndpointHandlersProvider interface {
335335
GetRFC8628UserAuthorizeEndpointHandlers(ctx context.Context) RFC8628UserAuthorizeEndpointHandlers
336336
}
337337

338+
type RFC8693ConfigProvider interface {
339+
GetTokenTypes(ctx context.Context) map[string]RFC8693TokenType
340+
341+
GetDefaultRequestedTokenType(ctx context.Context) string
342+
}
343+
338344
// UseLegacyErrorFormatProvider returns the provider for configuring whether to use the legacy error format.
339345
//
340346
// Deprecated: Do not use this flag anymore.

config_default.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,10 @@ type Config struct {
208208

209209
// IsPushedAuthorizeEnforced enforces pushed authorization request for /authorize
210210
IsPushedAuthorizeEnforced bool
211+
212+
RFC8693TokenTypes map[string]RFC8693TokenType
213+
214+
DefaultRequestedTokenType string
211215
}
212216

213217
func (c *Config) GetGlobalSecret(ctx context.Context) ([]byte, error) {
@@ -557,6 +561,14 @@ func (c *Config) EnforcePushedAuthorize(ctx context.Context) bool {
557561
return c.IsPushedAuthorizeEnforced
558562
}
559563

564+
func (c *Config) GetTokenTypes(ctx context.Context) map[string]RFC8693TokenType {
565+
return c.RFC8693TokenTypes
566+
}
567+
568+
func (c *Config) GetDefaultRequestedTokenType(ctx context.Context) string {
569+
return c.DefaultRequestedTokenType
570+
}
571+
560572
func (c *Config) GetRFC8628UserVerificationURL(_ context.Context) string {
561573
return c.RFC8628UserVerificationURL
562574
}
@@ -612,6 +624,7 @@ var (
612624
_ RevocationHandlersProvider = (*Config)(nil)
613625
_ PushedAuthorizeRequestHandlersProvider = (*Config)(nil)
614626
_ PushedAuthorizeRequestConfigProvider = (*Config)(nil)
627+
_ RFC8693ConfigProvider = (*Config)(nil)
615628
_ DeviceAuthorizeConfigProvider = (*Config)(nil)
616629
_ DeviceAuthorizeEndpointHandlersProvider = (*Config)(nil)
617630
_ RFC8628UserAuthorizeEndpointHandlersProvider = (*Config)(nil)

fosite.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ type Configurator interface {
159159
TokenEndpointHandlersProvider
160160
TokenIntrospectionHandlersProvider
161161
RevocationHandlersProvider
162+
PushedAuthorizeRequestHandlersProvider
163+
PushedAuthorizeRequestConfigProvider
164+
RFC8693ConfigProvider
162165
DeviceAuthorizeEndpointHandlersProvider
163166
RFC8628UserAuthorizeEndpointHandlersProvider
164167
DeviceAuthorizeConfigProvider

handler/oauth2/flow_generic_code_token.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ func (c *GenericCodeTokenEndpointHandler) PopulateTokenEndpointResponse(ctx cont
199199
}
200200

201201
responder.SetAccessToken(access)
202-
responder.SetTokenType("bearer")
202+
responder.SetTokenType(oauth2.BearerAccessToken)
203203
atLifespan := oauth2.GetEffectiveLifespan(requester.GetClient(), oauth2.GrantTypeAuthorizationCode, oauth2.AccessToken, c.Config.GetAccessTokenLifespan(ctx))
204204
responder.SetExpiresIn(getExpiresIn(requester, oauth2.AccessToken, atLifespan, time.Now().UTC()))
205205
responder.SetScopes(requester.GetGrantedScopes())

handler/oauth2/revocation.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,15 @@ type RevocationTokenLookupFunc func(ctx context.Context, token string) (requeste
8181

8282
func (r *TokenRevocationHandler) getRevokeRefreshTokensExplicitly(ctx context.Context, client oauth2.Client) bool {
8383
var (
84-
rfrrtec oauth2.RevokeFlowRevokeRefreshTokensExplicitClient
85-
ok bool
84+
c oauth2.RevokeFlowRevokeRefreshTokensExplicitClient
85+
ok bool
8686
)
8787

88-
if rfrrtec, ok = client.(oauth2.RevokeFlowRevokeRefreshTokensExplicitClient); !ok {
88+
if c, ok = client.(oauth2.RevokeFlowRevokeRefreshTokensExplicitClient); !ok {
8989
return r.Config.GetRevokeRefreshTokensExplicitly(ctx)
9090
}
9191

92-
if ok = rfrrtec.GetRevokeRefreshTokensExplicitly(ctx); ok || r.Config.GetEnforceRevokeFlowRevokeRefreshTokensExplicitClient(ctx) {
92+
if ok = c.GetRevokeRefreshTokensExplicitly(ctx); ok || r.Config.GetEnforceRevokeFlowRevokeRefreshTokensExplicitClient(ctx) {
9393
return ok
9494
}
9595

handler/openid/flow_device_authorization_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func TestOpenIDConnectDeviceAuthorizeHandler_PopulateRFC8628UserAuthorizeEndpoin
2929
}
3030
j := &DefaultStrategy{
3131
Signer: &jwt.DefaultSigner{
32-
GetPrivateKey: func(ctx context.Context) (interface{}, error) {
32+
GetPrivateKey: func(ctx context.Context) (any, error) {
3333
return key, nil
3434
},
3535
},
@@ -130,7 +130,7 @@ func TestOpenIDConnectDeviceAuthorizeHandler_PopulateTokenEndpointResponse(t *te
130130
}
131131
j := &DefaultStrategy{
132132
Signer: &jwt.DefaultSigner{
133-
GetPrivateKey: func(ctx context.Context) (interface{}, error) {
133+
GetPrivateKey: func(ctx context.Context) (any, error) {
134134
return key, nil
135135
},
136136
},

handler/openid/strategy.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@ import (
88
"time"
99

1010
"authelia.com/provider/oauth2"
11+
"authelia.com/provider/oauth2/token/jwt"
1112
)
1213

1314
type OpenIDConnectTokenStrategy interface {
1415
GenerateIDToken(ctx context.Context, lifespan time.Duration, requester oauth2.Requester) (token string, err error)
1516
}
17+
18+
type TokenValidationStrategy interface {
19+
ValidateIDToken(ctx context.Context, requester oauth2.Requester, token string) (jwt.MapClaims, error)
20+
}
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
package rfc8693
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
"github.com/pkg/errors"
8+
9+
"authelia.com/provider/oauth2"
10+
hoauth2 "authelia.com/provider/oauth2/handler/oauth2"
11+
"authelia.com/provider/oauth2/internal/consts"
12+
"authelia.com/provider/oauth2/internal/errorsx"
13+
"authelia.com/provider/oauth2/storage"
14+
)
15+
16+
type AccessTokenTypeHandler struct {
17+
Config oauth2.RFC8693ConfigProvider
18+
AccessTokenLifespan time.Duration
19+
RefreshTokenLifespan time.Duration
20+
RefreshTokenScopes []string
21+
hoauth2.CoreStrategy
22+
ScopeStrategy oauth2.ScopeStrategy
23+
Storage
24+
}
25+
26+
// HandleTokenEndpointRequest implements https://tools.ietf.org/html/rfc6749#section-4.3.2
27+
func (c *AccessTokenTypeHandler) HandleTokenEndpointRequest(ctx context.Context, request oauth2.AccessRequester) error {
28+
if !c.CanHandleTokenEndpointRequest(ctx, request) {
29+
return errorsx.WithStack(oauth2.ErrUnknownRequest)
30+
}
31+
32+
session, _ := request.GetSession().(Session)
33+
if session == nil {
34+
return errorsx.WithStack(oauth2.ErrServerError.WithDebug("Failed to perform token exchange because the session is not of the right type."))
35+
}
36+
37+
form := request.GetRequestForm()
38+
if form.Get(consts.FormParameterSubjectTokenType) != consts.TokenTypeRFC8693AccessToken && form.Get(consts.FormParameterActorTokenType) != consts.TokenTypeRFC8693AccessToken {
39+
return nil
40+
}
41+
42+
if form.Get(consts.FormParameterActorTokenType) == consts.TokenTypeRFC8693AccessToken {
43+
token := form.Get(consts.FormParameterActorToken)
44+
if _, unpacked, err := c.validate(ctx, request, token); err != nil {
45+
return err
46+
} else {
47+
session.SetActorToken(unpacked)
48+
}
49+
}
50+
51+
if form.Get(consts.FormParameterSubjectTokenType) == consts.TokenTypeRFC8693AccessToken {
52+
token := form.Get(consts.FormParameterSubjectToken)
53+
if subjectTokenSession, unpacked, err := c.validate(ctx, request, token); err != nil {
54+
return err
55+
} else {
56+
session.SetSubjectToken(unpacked)
57+
session.SetSubject(subjectTokenSession.GetSubject())
58+
}
59+
}
60+
61+
return nil
62+
}
63+
64+
// PopulateTokenEndpointResponse implements https://tools.ietf.org/html/rfc6749#section-4.3.3
65+
func (c *AccessTokenTypeHandler) PopulateTokenEndpointResponse(ctx context.Context, request oauth2.AccessRequester, responder oauth2.AccessResponder) error {
66+
if !c.CanHandleTokenEndpointRequest(ctx, request) {
67+
return errorsx.WithStack(oauth2.ErrUnknownRequest)
68+
}
69+
70+
session, _ := request.GetSession().(Session)
71+
if session == nil {
72+
return errorsx.WithStack(oauth2.ErrServerError.WithDebug("Failed to perform token exchange because the session is not of the right type."))
73+
}
74+
75+
form := request.GetRequestForm()
76+
requestedTokenType := form.Get(consts.FormParameterRequestedTokenType)
77+
if requestedTokenType == "" {
78+
requestedTokenType = c.Config.GetDefaultRequestedTokenType(ctx)
79+
}
80+
81+
if requestedTokenType != consts.TokenTypeRFC8693AccessToken {
82+
return nil
83+
}
84+
85+
if err := c.issue(ctx, request, responder); err != nil {
86+
return err
87+
}
88+
89+
return nil
90+
}
91+
92+
// CanSkipClientAuth indicates if client auth can be skipped
93+
func (c *AccessTokenTypeHandler) CanSkipClientAuth(ctx context.Context, requester oauth2.AccessRequester) bool {
94+
return false
95+
}
96+
97+
// CanHandleTokenEndpointRequest indicates if the token endpoint request can be handled
98+
func (c *AccessTokenTypeHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester oauth2.AccessRequester) bool {
99+
// grant_type REQUIRED.
100+
// Value MUST be set to "password".
101+
return requester.GetGrantTypes().ExactOne(consts.GrantTypeOAuthTokenExchange)
102+
}
103+
104+
func (c *AccessTokenTypeHandler) validate(ctx context.Context, request oauth2.AccessRequester, token string) (oauth2.Session, map[string]any, error) {
105+
session, _ := request.GetSession().(Session)
106+
if session == nil {
107+
return nil, nil, errorsx.WithStack(oauth2.ErrServerError.WithDebug(
108+
"Failed to perform token exchange because the session is not of the right type."))
109+
}
110+
111+
client := request.GetClient()
112+
113+
sig := c.CoreStrategy.AccessTokenSignature(ctx, token)
114+
or, err := c.Storage.GetAccessTokenSession(ctx, sig, request.GetSession())
115+
if err != nil {
116+
return nil, nil, errors.WithStack(oauth2.ErrInvalidRequest.WithHint("Token is not valid or has expired.").WithDebugError(err))
117+
} else if err := c.CoreStrategy.ValidateAccessToken(ctx, or, token); err != nil {
118+
return nil, nil, err
119+
}
120+
121+
subjectTokenClientID := or.GetClient().GetID()
122+
// forbid original subjects client to exchange its own token
123+
if client.GetID() == subjectTokenClientID {
124+
return nil, nil, errors.WithStack(oauth2.ErrRequestForbidden.WithHint("Clients are not allowed to perform a token exchange on their own tokens."))
125+
}
126+
127+
// Check if the client is allowed to exchange this token
128+
if subjectTokenClient, ok := or.GetClient().(Client); ok {
129+
allowed := subjectTokenClient.TokenExchangeAllowed(client)
130+
if !allowed {
131+
return nil, nil, errors.WithStack(oauth2.ErrRequestForbidden.WithHintf(
132+
"The OAuth 2.0 client is not permitted to exchange a subject token issued to client %s", subjectTokenClientID))
133+
}
134+
}
135+
136+
// Scope check
137+
for _, scope := range request.GetRequestedScopes() {
138+
if !c.ScopeStrategy(or.GetGrantedScopes(), scope) {
139+
return nil, nil, errors.WithStack(oauth2.ErrInvalidScope.WithHintf("The subject token is not granted '%s' and so this scope cannot be requested.", scope))
140+
}
141+
}
142+
143+
// Convert to flat session with only access token claims
144+
claims := session.AccessTokenClaimsMap()
145+
claims[consts.ClaimClientIdentifier] = or.GetClient().GetID()
146+
claims[consts.ClaimScope] = or.GetGrantedScopes()
147+
claims[consts.ClaimAudience] = or.GetGrantedAudience()
148+
149+
return or.GetSession(), claims, nil
150+
}
151+
152+
func (c *AccessTokenTypeHandler) issue(ctx context.Context, request oauth2.AccessRequester, response oauth2.AccessResponder) error {
153+
request.GetSession().SetExpiresAt(oauth2.AccessToken, time.Now().UTC().Add(c.AccessTokenLifespan))
154+
155+
token, signature, err := c.CoreStrategy.GenerateAccessToken(ctx, request)
156+
if err != nil {
157+
return err
158+
} else if err := c.Storage.CreateAccessTokenSession(ctx, signature, request.Sanitize([]string{})); err != nil {
159+
return err
160+
}
161+
162+
issueRefreshToken := c.canIssueRefreshToken(request)
163+
if issueRefreshToken {
164+
request.GetSession().SetExpiresAt(oauth2.RefreshToken, time.Now().UTC().Add(c.RefreshTokenLifespan).Round(time.Second))
165+
refresh, refreshSignature, err := c.CoreStrategy.GenerateRefreshToken(ctx, request)
166+
if err != nil {
167+
return errors.WithStack(oauth2.ErrServerError.WithDebugError(err))
168+
}
169+
170+
if refreshSignature != "" {
171+
if err := c.Storage.CreateRefreshTokenSession(ctx, refreshSignature, request.Sanitize([]string{})); err != nil {
172+
if rollBackTxnErr := storage.MaybeRollbackTx(ctx, c.Storage); rollBackTxnErr != nil {
173+
err = rollBackTxnErr
174+
}
175+
return errors.WithStack(oauth2.ErrServerError.WithDebugError(err))
176+
}
177+
}
178+
179+
response.SetExtra(consts.FormParameterRefreshToken, refresh)
180+
}
181+
182+
response.SetAccessToken(token)
183+
response.SetTokenType(oauth2.BearerAccessToken)
184+
response.SetExpiresIn(c.getExpiresIn(request, oauth2.AccessToken, c.AccessTokenLifespan, time.Now().UTC()))
185+
response.SetScopes(request.GetGrantedScopes())
186+
187+
return nil
188+
}
189+
190+
func (c *AccessTokenTypeHandler) canIssueRefreshToken(request oauth2.Requester) bool {
191+
// Require one of the refresh token scopes, if set.
192+
if len(c.RefreshTokenScopes) > 0 && !request.GetGrantedScopes().HasOneOf(c.RefreshTokenScopes...) {
193+
return false
194+
}
195+
// Do not issue a refresh token to clients that cannot use the refresh token grant type.
196+
if !request.GetClient().GetGrantTypes().Has(consts.GrantTypeRefreshToken) {
197+
return false
198+
}
199+
return true
200+
}
201+
202+
func (c *AccessTokenTypeHandler) getExpiresIn(r oauth2.Requester, key oauth2.TokenType, defaultLifespan time.Duration, now time.Time) time.Duration {
203+
if r.GetSession().GetExpiresAt(key).IsZero() {
204+
return defaultLifespan
205+
}
206+
return time.Duration(r.GetSession().GetExpiresAt(key).UnixNano() - now.UnixNano())
207+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package rfc8693
2+
3+
import (
4+
"context"
5+
6+
"github.com/pkg/errors"
7+
8+
"authelia.com/provider/oauth2"
9+
"authelia.com/provider/oauth2/internal/consts"
10+
"authelia.com/provider/oauth2/internal/errorsx"
11+
)
12+
13+
type ActorTokenValidationHandler struct{}
14+
15+
// HandleTokenEndpointRequest implements https://tools.ietf.org/html/rfc6749#section-4.3.2
16+
func (c *ActorTokenValidationHandler) HandleTokenEndpointRequest(ctx context.Context, request oauth2.AccessRequester) error {
17+
if !c.CanHandleTokenEndpointRequest(ctx, request) {
18+
return errorsx.WithStack(oauth2.ErrUnknownRequest)
19+
}
20+
21+
client := request.GetClient()
22+
session, _ := request.GetSession().(Session)
23+
if session == nil {
24+
return errorsx.WithStack(oauth2.ErrServerError.WithDebug("Failed to perform token exchange because the session is not of the right type."))
25+
}
26+
27+
// Validate that the actor or client is allowed to make this request
28+
subjectTokenObject := session.GetSubjectToken()
29+
if mayAct, _ := subjectTokenObject[consts.ClaimAuthorizedActor].(map[string]any); mayAct != nil {
30+
actorTokenObject := session.GetActorToken()
31+
if actorTokenObject == nil {
32+
actorTokenObject = map[string]any{
33+
consts.ClaimSubject: client.GetID(),
34+
consts.ClaimClientIdentifier: client.GetID(),
35+
}
36+
}
37+
38+
for k, v := range mayAct {
39+
if actorTokenObject[k] != v {
40+
return errors.WithStack(oauth2.ErrInvalidRequest.WithHint("The actor or client is not authorized to act on behalf of the subject."))
41+
}
42+
}
43+
}
44+
45+
return nil
46+
}
47+
48+
// PopulateTokenEndpointResponse implements https://tools.ietf.org/html/rfc6749#section-4.3.3
49+
func (c *ActorTokenValidationHandler) PopulateTokenEndpointResponse(ctx context.Context, request oauth2.AccessRequester, responder oauth2.AccessResponder) error {
50+
return nil
51+
}
52+
53+
// CanSkipClientAuth indicates if client auth can be skipped
54+
func (c *ActorTokenValidationHandler) CanSkipClientAuth(ctx context.Context, requester oauth2.AccessRequester) bool {
55+
return false
56+
}
57+
58+
// CanHandleTokenEndpointRequest indicates if the token endpoint request can be handled
59+
func (c *ActorTokenValidationHandler) CanHandleTokenEndpointRequest(ctx context.Context, requester oauth2.AccessRequester) bool {
60+
// grant_type REQUIRED.
61+
// Value MUST be set to "password".
62+
return requester.GetGrantTypes().ExactOne(consts.GrantTypeOAuthTokenExchange)
63+
}

0 commit comments

Comments
 (0)