Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

handler: Adds PKCE implementation for none and S256 #246

Merged
merged 2 commits into from
Feb 7, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ Apache License
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright 2016 Ory GmbH & Aeneas Rekkas
Copyright 2016 - 2018 Ory GmbH & Aeneas Rekkas

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ includes all flows: code, implicit, hybrid.
This library considered and implemented:
* [The OAuth 2.0 Authorization Framework](https://tools.ietf.org/html/rfc6749)
* [OAuth 2.0 Multiple Response Type Encoding Practices](https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html)
* [OAuth 2.0 Threat Model and Security Considerations](https://tools.ietf.org/html/rfc6819) (partially)
* [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html) (partially)
* [OAuth 2.0 Threat Model and Security Considerations](https://tools.ietf.org/html/rfc6819)
* [Proof Key for Code Exchange by OAuth Public Clients](https://tools.ietf.org/html/rfc7636)
* [OAuth 2.0 for Native Apps](https://tools.ietf.org/html/rfc8252)
* [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)

OAuth2 and OpenID Connect are difficult protocols. If you want quick wins, we strongly encourage you to look at [Hydra](https://github.com/ory-am/hydra).
Hydra is a secure, high performance, cloud native OAuth2 and OpenID Connect service that integrates with every authentication method
Expand Down
2 changes: 2 additions & 0 deletions compose/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ func ComposeAllEnabled(config *Config, storage interface{}, secret []byte, key *
OAuth2RefreshTokenGrantFactory,
OAuth2ResourceOwnerPasswordCredentialsFactory,

OAuth2PKCEFactory,

OpenIDConnectExplicitFactory,
OpenIDConnectImplicitFactory,
OpenIDConnectHybridFactory,
Expand Down
16 changes: 16 additions & 0 deletions compose/compose_pkce.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package compose

import (
"github.com/ory/fosite/handler/oauth2"
"github.com/ory/fosite/handler/pkce"
)

// OAuth2PKCEFactory creates a PKCE handler.
func OAuth2PKCEFactory(config *Config, storage interface{}, strategy interface{}) interface{} {
return &pkce.Handler{
AuthorizeCodeStrategy: strategy.(oauth2.AuthorizeCodeStrategy),
CoreStorage: storage.(oauth2.CoreStorage),
Force: config.EnforcePKCE,
EnablePlainChallengeMethod: config.EnablePKCEPlainChallengeMethod,
}
}
6 changes: 6 additions & 0 deletions compose/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ type Config struct {

// ScopeStrategy sets the scope strategy that should be supported, for example fosite.WildcardScopeStrategy.
ScopeStrategy fosite.ScopeStrategy

// EnforcePKCE, if set to true, requires public clients to perform authorize code flows with PKCE. Defaults to false.
EnforcePKCE bool

// EnablePKCEPlainChallengeMethod sets whether or not to allow the plain challenge method (S256 should be used whenever possible, plain is really discouraged). Defaults to false.
EnablePKCEPlainChallengeMethod bool
}

// GetScopeStrategy returns the scope strategy to be used. Defaults to glob scope strategy.
Expand Down
6 changes: 6 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,3 +252,9 @@ func (e *RFC6749Error) WithDebug(debug string) *RFC6749Error {
err.Debug = debug
return &err
}

func (e *RFC6749Error) WithDescription(description string) *RFC6749Error {
err := *e
err.Description = description
return &err
}
180 changes: 180 additions & 0 deletions handler/pkce/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package pkce

import (
"context"
"crypto/sha256"
"encoding/base64"

"github.com/ory/fosite"
"github.com/ory/fosite/handler/oauth2"
"github.com/pkg/errors"
)

type Handler struct {
// If set to true, public clients must use PKCE.
Force bool

// Whether or not to allow the plain challenge method (S256 should be used whenever possible, plain is really discouraged).
EnablePlainChallengeMethod bool

AuthorizeCodeStrategy oauth2.AuthorizeCodeStrategy
CoreStorage oauth2.CoreStorage
}

func (c *Handler) HandleAuthorizeEndpointRequest(ctx context.Context, ar fosite.AuthorizeRequester, resp fosite.AuthorizeResponder) error {
// This let's us define multiple response types, for example open id connect's id_token
if !ar.GetResponseTypes().Exact("code") {
return nil
}

if !ar.GetClient().IsPublic() {
return nil
}

challenge := ar.GetRequestForm().Get("code_challenge")
method := ar.GetRequestForm().Get("code_challenge_method")
return c.validate(challenge, method)
}

func (c *Handler) validate(challenge, method string) error {
if c.Force && challenge == "" {
//If the server requires Proof Key for Code Exchange (PKCE) by OAuth
//public clients and the client does not send the "code_challenge" in
//the request, the authorization endpoint MUST return the authorization
//error response with the "error" value set to "invalid_request". The
//"error_description" or the response of "error_uri" SHOULD explain the
//nature of error, e.g., code challenge required.

return errors.WithStack(fosite.ErrInvalidRequest.
WithDescription("Public clients must include a code_challenge when performing the authorize code flow, but it is missing.").
WithDebug("The server is configured in a way that enforces PKCE for public clients."))
}

if !c.Force && challenge == "" {
return nil
}

//If the server supporting PKCE does not support the requested
//transformation, the authorization endpoint MUST return the
//authorization error response with "error" value set to
//"invalid_request". The "error_description" or the response of
//"error_uri" SHOULD explain the nature of error, e.g., transform
//algorithm not supported.
switch method {
case "S256":
break
case "plain":
fallthrough
case "":
if !c.EnablePlainChallengeMethod {
return errors.WithStack(fosite.ErrInvalidRequest.
WithDescription("Public clients must use code_challenge_method=S256, plain is not allowed.").
WithDebug("The server is configured in a way that enforces PKCE S256 as challenge method for public clients."))
}
break
default:
return errors.WithStack(fosite.ErrInvalidRequest.
WithDescription("The code_challenge_method is not supported, use S256 instead."))
}
return nil
}

func (c *Handler) HandleTokenEndpointRequest(ctx context.Context, request fosite.AccessRequester) error {
// This let's us define multiple response types, for example open id connect's id_token
if !request.GetGrantTypes().Exact("authorization_code") {
return errors.WithStack(fosite.ErrUnknownRequest)
}

if !request.GetClient().IsPublic() {
return errors.WithStack(fosite.ErrUnknownRequest)
}

code := request.GetRequestForm().Get("code")
signature := c.AuthorizeCodeStrategy.AuthorizeCodeSignature(code)
authorizeRequest, err := c.CoreStorage.GetAuthorizeCodeSession(ctx, signature, request.GetSession())
if err != nil {
return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error()))
}

//code_verifier
//REQUIRED. Code verifier
//
//The "code_challenge_method" is bound to the Authorization Code when
//the Authorization Code is issued. That is the method that the token
//endpoint MUST use to verify the "code_verifier".
verifier := request.GetRequestForm().Get("code_verifier")
challenge := authorizeRequest.GetRequestForm().Get("code_challenge")
method := authorizeRequest.GetRequestForm().Get("code_challenge_method")
if err := c.validate(challenge, method); err != nil {
return err
}

if !c.Force && challenge == "" && verifier == "" {
return nil
}

//Upon receipt of the request at the token endpoint, the server
//verifies it by calculating the code challenge from the received
//"code_verifier" and comparing it with the previously associated
//"code_challenge", after first transforming it according to the
//"code_challenge_method" method specified by the client.
//
// If the "code_challenge_method" from Section 4.3 was "S256", the
//received "code_verifier" is hashed by SHA-256, base64url-encoded, and
//then compared to the "code_challenge", i.e.:
//
//BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) == code_challenge
//
//If the "code_challenge_method" from Section 4.3 was "plain", they are
//compared directly, i.e.:
//
//code_verifier == code_challenge.
//
// If the values are equal, the token endpoint MUST continue processing
//as normal (as defined by OAuth 2.0 [RFC6749]). If the values are not
//equal, an error response indicating "invalid_grant" as described in
//Section 5.2 of [RFC6749] MUST be returned.
switch method {
case "S256":
verifierLength := base64.RawURLEncoding.DecodedLen(len(verifier))

// NOTE: The code verifier SHOULD have enough entropy to make it
// impractical to guess the value. It is RECOMMENDED that the output of
// a suitable random number generator be used to create a 32-octet
// sequence. The octet sequence is then base64url-encoded to produce a
// 43-octet URL safe string to use as the code verifier.
if verifierLength < 32 {
return errors.WithStack(fosite.ErrInsufficientEntropy.
WithDebug("The PKCE code verifier must contain at least 32 octets."))
}

verifierBytes := make([]byte, verifierLength)
if _, err := base64.RawURLEncoding.Decode(verifierBytes, []byte(verifier)); err != nil {
return errors.WithStack(fosite.ErrInvalidGrant.WithDescription("Unable to decode code_verifier using base64 url decoding without padding.").WithDebug(err.Error()))
}

hash := sha256.New()
if _, err := hash.Write([]byte(verifier)); err != nil {
return errors.WithStack(fosite.ErrServerError.WithDebug(err.Error()))
}

if base64.RawURLEncoding.EncodeToString(hash.Sum([]byte{})) != challenge {
return errors.WithStack(fosite.ErrInvalidGrant.
WithDebug("The PKCE code challenge did not match the code verifier."))
}
break
case "plain":
fallthrough
default:
if verifier != challenge {
return errors.WithStack(fosite.ErrInvalidGrant.
WithDebug("The PKCE code challenge did not match the code verifier."))
}
}

return nil
}

func (c *Handler) PopulateTokenEndpointResponse(ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder) error {
return nil
}
Loading