Skip to content

Commit

Permalink
fix: scope parameter ignored in refresh flow (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
james-d-elliott authored Dec 21, 2023
1 parent 5f46c1a commit 6584d34
Show file tree
Hide file tree
Showing 52 changed files with 842 additions and 479 deletions.
41 changes: 26 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

This is a hard fork of [ORY Fosite](https://github.com/ory/fosite) under the
[Apache 2.0 License](LICENSE) for the purpose of performing self-maintenance of
this critical dependency.
this critical Authelia dependency.

We however:

- Acknowledge the amazing hard work of the ORY developers in making such an
amazing framework that we can do this with.
- Plan to continue to contribute back to te ORY Fosite and related projects.
- Plan to continue to contribute back to te ORY fosite and related projects.
- Have ensured the licensing is unchanged in this fork of the library.
- Do not have a formal affiliation with ORY and individuals utilizing this
library should not allow their usage to be a reflection on ORY as this library
Expand All @@ -21,18 +21,33 @@ following list of differences:

- [x] Module path changed from `github.com/ory/fosite` to
`authelia.com/provider/oauth2`.
- Documentation:
- [ ] Add spec support documentation
- Overhaul testing:
- [ ] Ensure all tests and subtests are well named
- [ ] Ensure all tests are simplified where possible
- [ ] Restore/Implement conformance tests
- Rename interfaces and implementations:
- [x] `OAuth2Provider` to `Provider`.
- [ ] `Fosite` to `TBA`.
- [x] Minimum dependency is go version 1.21.
- [x] Minimum dependency is go version 1.21
- [ ] Replace string values with constants where applicable
- [ ] Simplify the internal JWT logic to leverage `github.com/golang-jwt/jwt/v5`
- [ ] Implement internal JWKS logic
- Fixes:
- [x] Basic Scheme Rejects Special Characters
2314625eb1f21987a9199fb1cdf6da6cee4df965
- [x] RFC9068 must condition ignored f4652d60c850d167da00e2d2fe9096776eff9465
- [ ] Refresh Flow ignores requested scope
- [ ] Refresh Flow does not set original request ID early enough
- [ ] PKCE Flow session generated needlessly
- [ ] OpenID Flows ignore empty redirect uri
<sup>[commit](https://github.com/authelia/oauth2-provider/commit/2314625eb1f21987a9199fb1cdf6da6cee4df965)</sup>
- [x] RFC9068 must condition ignored
<sup>[commit](https://github.com/authelia/oauth2-provider/commit/f4652d60c850d167da00e2d2fe9096776eff9465)</sup>
- Refresh Flow:
- [x] Requested scope ignored
- [x] Original request id not set early enough
- PKCE Flow
- [ ] Session generated needlessly
- [ ] Failure to fetch session causes an error even when not enforced
- OpenID Flows:
- [x] Absence of Redirect URI does not result in an error
<sup>[commit](https://github.com/authelia/oauth2-provider/commit/f4652d60c850d167da00e2d2fe9096776eff9465)</sup>
- [ ] Decode id_token_hint with correct signer
- [ ] Write Revocation Response does not correctly error
- [ ] Invalid Token base 64 error not mapped to RFC
Expand All @@ -42,6 +57,7 @@ following list of differences:
- Features:
- [ ] Customizable Token Prefix
- [ ] JWE support for Client Authentication and Issuance
- [ ] UserInfo support
- [ ] JARM support
- [ ] Revocation Flow per policy can decide to revoke Refresh Tokens on
request
Expand All @@ -63,11 +79,6 @@ following list of differences:
- [x] `github.com/form3tech-oss/jwt-go`
- [x] `github.com/dgrijalva/jwt-go`
- Migration of the following dependencies:
- [ ] `github.com/go-jose/go-jose/v3` => `github.com/golang-jwt/jwt/v5`
- [x] `github.com/golang/mock` => `github.com/uber-go/mock`
- [x] `github.com/cristalhq/jwt/v4` => `github.com/golang-jwt/jwt/v5`

## TODO

- Consolidate JWT and JOSE dependencies
- Remove unecessary dependencies and/or abstract them
- Apply downstream fixes
2 changes: 1 addition & 1 deletion access_error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func TestWriteAccessError(t *testing.T) {
}

func TestWriteAccessError_RFC6749(t *testing.T) {
// https://tools.ietf.org/html/rfc6749#section-5.2
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2

config := new(Config)
provider := &Fosite{Config: config}
Expand Down
19 changes: 19 additions & 0 deletions access_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,22 @@ func NewAccessRequest(session Session) *AccessRequest {
func (a *AccessRequest) GetGrantTypes() Arguments {
return a.GrantTypes
}

func (a *AccessRequest) SetGrantedScopes(scopes Arguments) {
a.GrantedScope = scopes
}

func (a *AccessRequest) SanitizeRestoreRefreshTokenOriginalRequester(requester Requester) Requester {
r := a.Sanitize(nil).(*Request)

ar := &AccessRequest{
Request: *r,
}

ar.SetID(requester.GetID())

ar.SetRequestedScopes(requester.GetRequestedScopes())
ar.SetGrantedScopes(requester.GetGrantedScopes())

return ar
}
4 changes: 2 additions & 2 deletions access_request_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
)

// Implements
// - https://tools.ietf.org/html/rfc6749#section-2.3.1
// - https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1
// Clients in possession of a client password MAY use the HTTP Basic
// authentication scheme as defined in [RFC2617] to authenticate with
// the authorization server. The client identifier is encoded using the
Expand All @@ -31,7 +31,7 @@ import (
// password-based HTTP authentication schemes). The parameters can only
// be transmitted in the request-body and MUST NOT be included in the
// request URI.
// - https://tools.ietf.org/html/rfc6749#section-3.2.1
// - https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1
// - Confidential clients or other clients issued client credentials MUST
// authenticate with the authorization server as described in
// Section 2.3 when making requests to the token endpoint.
Expand Down
10 changes: 6 additions & 4 deletions access_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package oauth2
import (
"strings"
"time"

"authelia.com/provider/oauth2/internal/consts"
)

func NewAccessResponse() *AccessResponse {
Expand All @@ -21,11 +23,11 @@ type AccessResponse struct {
}

func (a *AccessResponse) SetScopes(scopes Arguments) {
a.SetExtra("scope", strings.Join(scopes, " "))
a.SetExtra(consts.AccessResponseScope, strings.Join(scopes, " "))
}

func (a *AccessResponse) SetExpiresIn(expiresIn time.Duration) {
a.SetExtra("expires_in", int64(expiresIn/time.Second))
a.SetExtra(consts.AccessResponseExpiresIn, int64(expiresIn/time.Second))
}

func (a *AccessResponse) SetExtra(key string, value any) {
Expand Down Expand Up @@ -53,7 +55,7 @@ func (a *AccessResponse) GetTokenType() string {
}

func (a *AccessResponse) ToMap() map[string]any {
a.Extra["access_token"] = a.GetAccessToken()
a.Extra["token_type"] = a.GetTokenType()
a.Extra[consts.AccessResponseAccessToken] = a.GetAccessToken()
a.Extra[consts.AccessResponseTokenType] = a.GetTokenType()
return a.Extra
}
13 changes: 7 additions & 6 deletions access_response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,21 @@ import (
"github.com/stretchr/testify/assert"

. "authelia.com/provider/oauth2"
"authelia.com/provider/oauth2/internal/consts"
)

func TestAccessResponse(t *testing.T) {
ar := NewAccessResponse()
ar.SetAccessToken("access")
ar.SetTokenType("bearer")
ar.SetExtra("access_token", "invalid")
ar.SetTokenType(BearerAccessToken)
ar.SetExtra(consts.AccessResponseAccessToken, "invalid")
ar.SetExtra("foo", "bar")
assert.Equal(t, "access", ar.GetAccessToken())
assert.Equal(t, "bearer", ar.GetTokenType())
assert.Equal(t, BearerAccessToken, ar.GetTokenType())
assert.Equal(t, "bar", ar.GetExtra("foo"))
assert.Equal(t, map[string]any{
"access_token": "access",
"token_type": "bearer",
"foo": "bar",
consts.AccessResponseAccessToken: "access",
consts.AccessResponseTokenType: BearerAccessToken,
"foo": "bar",
}, ar.ToMap())
}
4 changes: 2 additions & 2 deletions authorize_error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ import (
)

// Test for
// - https://tools.ietf.org/html/rfc6749#section-4.1.2.1
// - https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1
// If the request fails due to a missing, invalid, or mismatching
// redirection URI, or if the client identifier is missing or invalid,
// the authorization server SHOULD inform the resource owner of the
// error and MUST NOT automatically redirect the user-agent to the
// invalid redirection URI.
// - https://tools.ietf.org/html/rfc6749#section-3.1.2
// - https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2
// The redirection endpoint URI MUST be an absolute URI as defined by
// [RFC3986] Section 4.3. The endpoint URI MAY include an
// "application/x-www-form-urlencoded" formatted (per Appendix B) query
Expand Down
14 changes: 7 additions & 7 deletions authorize_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ var DefaultFormPostTemplate = template.Must(template.New("form_post").Parse(`<ht
//
// Considered specifications
//
// - https://tools.ietf.org/html/rfc6749#section-3.1.2.3
// - https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2.3
// If multiple redirection URIs have been registered, if only part of
// the redirection URI has been registered, or if no redirection URI has
// been registered, the client MUST include a redirection URI with the
Expand All @@ -50,7 +50,7 @@ var DefaultFormPostTemplate = template.Must(template.New("form_post").Parse(`<ht
// redirection URI, the authorization server MUST compare the two URIs
// using simple string comparison as defined in [RFC3986] Section 6.2.1.
//
// * https://tools.ietf.org/html/rfc6819#section-4.4.1.7
// * https://datatracker.ietf.org/doc/html/rfc6819#section-4.4.1.7
// - The authorization server may also enforce the usage and validation
// of pre-registered redirect URIs (see Section 5.2.3.5). This will
// allow for early recognition of authorization "code" disclosure to
Expand Down Expand Up @@ -87,7 +87,7 @@ func MatchRedirectURIWithClientRedirectURIs(rawurl string, client Client) (*url.
// an IPv6 URI http://[::1] a client is allowed to request a dynamic port and the server MUST accept
// it as a valid redirection uri.
//
// https://tools.ietf.org/html/rfc8252#section-7.3
// https://datatracker.ietf.org/doc/html/rfc8252#section-7.3
// Native apps that are able to open a port on the loopback network
// interface without needing special permissions (typically, those on
// desktop operating systems) can use the loopback interface to receive
Expand Down Expand Up @@ -127,7 +127,7 @@ func isMatchingAsLoopback(requested *url.URL, registeredURI string) bool {
// Loopback redirect URIs use the "http" scheme and are constructed with
// the loopback IP literal and whatever port the client is listening on.
//
// Source: https://tools.ietf.org/html/rfc8252#section-7.3
// Source: https://datatracker.ietf.org/doc/html/rfc8252#section-7.3
if requested.Scheme == "http" &&
isLoopbackAddress(requested.Host) &&
registered.Hostname() == requested.Hostname() &&
Expand All @@ -152,12 +152,12 @@ func isLoopbackAddress(address string) bool {

// IsValidRedirectURI validates a redirect_uri as specified in:
//
// * https://tools.ietf.org/html/rfc6749#section-3.1.2
// * https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2
// - The redirection endpoint URI MUST be an absolute URI as defined by [RFC3986] Section 4.3.
// - The endpoint URI MUST NOT include a fragment component.
// - https://tools.ietf.org/html/rfc3986#section-4.3
// - https://datatracker.ietf.org/doc/html/rfc3986#section-4.3
// absolute-URI = scheme ":" hier-part [ "?" query ]
// - https://tools.ietf.org/html/rfc6819#section-5.1.1
// - https://datatracker.ietf.org/doc/html/rfc6819#section-5.1.1
func IsValidRedirectURI(redirectURI *url.URL) bool {
// We need to explicitly check for a scheme
if !urls.IsRequestURL(redirectURI.String()) {
Expand Down
4 changes: 2 additions & 2 deletions authorize_request_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ func (f *Fosite) validateAuthorizeScope(ctx context.Context, _ *http.Request, re
}

func (f *Fosite) validateResponseTypes(r *http.Request, request *AuthorizeRequest) error {
// https://tools.ietf.org/html/rfc6749#section-3.1.1
// https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.1
// Extension response types MAY contain a space-delimited (%x20) list of
// values, where the order of values does not matter (e.g., response
// type "a b" is the same as "b a"). The meaning of such composite
Expand Down Expand Up @@ -414,7 +414,7 @@ func (f *Fosite) newAuthorizeRequest(ctx context.Context, r *http.Request, isPAR
// The "state" parameter should be used to link the authorization
// request with the redirect URI used to deliver the access token (Section 5.3.5).
//
// https://tools.ietf.org/html/rfc6819#section-4.4.1.8
// https://datatracker.ietf.org/doc/html/rfc6819#section-4.4.1.8
// The "state" parameter should not be guessable
if len(request.State) < f.GetMinParameterEntropy(ctx) {
// We're assuming that using less then, by default, 8 characters for the state can not be considered "unguessable"
Expand Down
2 changes: 1 addition & 1 deletion authorize_write.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func (f *Fosite) WriteAuthorizeResponse(ctx context.Context, rw http.ResponseWri
}
}

// https://tools.ietf.org/html/rfc6749#section-4.1.1
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
// When a decision is established, the authorization server directs the
// user-agent to the provided client redirection URI using an HTTP
// redirection response, or by other means available to it via the
Expand Down
13 changes: 12 additions & 1 deletion client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
package oauth2

import (
"context"

"github.com/go-jose/go-jose/v3"

"authelia.com/provider/oauth2/internal/consts"
)

// Client represents a client or an app.
Expand Down Expand Up @@ -71,6 +75,13 @@ type OpenIDConnectClient interface {
GetTokenEndpointAuthSigningAlgorithm() string
}

// RefreshFlowScopeClient is a client which can be customized to ignore scopes that were not originally granted.
type RefreshFlowScopeClient interface {
Client

GetRefreshFlowIgnoreOriginalGrantedScopes(ctx context.Context) (ignoreOriginalGrantedScopes bool)
}

// ResponseModeClient represents a client capable of handling response_mode
type ResponseModeClient interface {
// GetResponseModes returns the response modes that client is allowed to send
Expand Down Expand Up @@ -140,7 +151,7 @@ func (c *DefaultClient) GetGrantTypes() Arguments {
// that it will restrict itself to using.
// If omitted, the default is that the Client will use only the authorization_code Grant Type.
if len(c.GrantTypes) == 0 {
return Arguments{"authorization_code"}
return Arguments{consts.GrantTypeAuthorizationCode}
}

return c.GrantTypes
Expand Down
6 changes: 4 additions & 2 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"testing"

"github.com/stretchr/testify/assert"

"authelia.com/provider/oauth2/internal/consts"
)

func TestDefaultClient(t *testing.T) {
Expand All @@ -30,8 +32,8 @@ func TestDefaultClient(t *testing.T) {

sc.GrantTypes = []string{}
sc.ResponseTypes = []string{}
assert.Equal(t, "code", sc.GetResponseTypes()[0])
assert.Equal(t, "authorization_code", sc.GetGrantTypes()[0])
assert.Equal(t, consts.ResponseTypeAuthorizationCodeFlow, sc.GetResponseTypes()[0])
assert.Equal(t, consts.GrantTypeAuthorizationCode, sc.GetGrantTypes()[0])

var _ ClientWithSecretRotation = sc
}
Expand Down
29 changes: 28 additions & 1 deletion errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ const (
errInsufficientEntropyName = "insufficient_entropy"
errInvalidTokenFormatName = "invalid_token"
errTokenSignatureMismatchName = "token_signature_mismatch"
errTokenExpiredName = "invalid_token" // https://tools.ietf.org/html/rfc6750#section-3.1
errTokenExpiredName = "invalid_token" // https://datatracker.ietf.org/doc/html/rfc6750#section-3.1
errScopeNotGrantedName = "scope_not_granted"
errTokenClaimName = "token_claim"
errTokenInactiveName = "token_inactive"
Expand Down Expand Up @@ -533,3 +533,30 @@ func (e *RFC6749Error) computeHintField() {

e.HintField = i18n.GetMessageOrDefault(e.catalog, e.hintIDField, e.lang, e.HintField, e.hintArgs...)
}

// ErrorToDebugRFC6749Error converts the provided error to a *DebugRFC6749Error provided it is not nil and can be
// cast as a *RFC6749Error.
func ErrorToDebugRFC6749Error(err error) (rfc error) {
if err == nil {
return nil
}

var e *RFC6749Error

if errors.As(err, &e) {
return &DebugRFC6749Error{e}
}

return err
}

// DebugRFC6749Error is a decorator type which makes the underlying *RFC6749Error expose debug information and
// show the full error description.
type DebugRFC6749Error struct {
*RFC6749Error
}

// Error implements the builtin error interface and shows the error with its debug info and description.
func (err *DebugRFC6749Error) Error() string {
return err.WithExposeDebug(true).GetDescription()
}
Loading

0 comments on commit 6584d34

Please sign in to comment.