Skip to content

Commit

Permalink
Implement PKCE code challenges as defined in RFC 7636
Browse files Browse the repository at this point in the history
This change adds support for code challenges when the OAuth2 code flow
is used. This additional security mechanism can be used to ensure that
the client completing the token request with a code is the client which
initiated the authorization. This change adds support for the code
challenge method S256 to the authorize and token requests as
optional parameters.

Reference: https://tools.ietf.org/html/rfc7636
  • Loading branch information
longsleep committed Mar 12, 2019
1 parent 205ffd6 commit f8c1f4a
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 1 deletion.
57 changes: 57 additions & 0 deletions oidc/code.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2019 Kopano and its licensors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

package oidc

import (
"crypto/sha256"
"encoding/base64"
"errors"
)

// Code challenge methods implemented by Konnect. See https://tools.ietf.org/html/rfc7636.
const (
PlainCodeChallengeMethod = "plain"
S256CodeChallengeMethod = "S256"
)

// ValidateCodeChallenge implements https://tools.ietf.org/html/rfc7636#section-4.6
// code challenge verification.
func ValidateCodeChallenge(challenge string, method string, verifier string) error {
var err error

switch method {
case PlainCodeChallengeMethod:
if challenge != verifier {
err = errors.New("invalid code challenge")
}
case "":
// We default to S256CodeChallengeMethod.
fallthrough
case S256CodeChallengeMethod:
// BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) == code_challenge
sum := sha256.Sum256([]byte(verifier))
if challenge != base64.URLEncoding.EncodeToString(sum[:]) {
err = errors.New("invalid code challenge")
}

default:
err = errors.New("transform algorithm not supported")
}

return err
}
24 changes: 24 additions & 0 deletions oidc/payload/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ type AuthenticationRequest struct {
RawRequestURI string `schema:"request_uri"`
RawRegistration string `schema:"registration"`

CodeChallenge string `schema:"code_challenge"`
CodeChallengeMethod string `schema:"code_challenge_method"`

Scopes map[string]bool `schema:"-"`
ResponseTypes map[string]bool `schema:"-"`
Prompts map[string]bool `schema:"-"`
Expand Down Expand Up @@ -249,6 +252,12 @@ func (ar *AuthenticationRequest) ApplyRequestObject(roc *RequestObjectClaims, me
if roc.RawRegistration != "" {
ar.RawRegistration = roc.RawRegistration
}
if roc.CodeChallengeMethod != "" {
ar.CodeChallengeMethod = roc.CodeChallengeMethod
}
if roc.CodeChallenge != "" {
ar.CodeChallenge = roc.CodeChallenge
}

return nil
}
Expand Down Expand Up @@ -287,6 +296,21 @@ func (ar *AuthenticationRequest) Validate(keyFunc jwt.Keyfunc) error {
return ar.NewError(oidc.ErrorOAuth2UnsupportedResponseType, "")
}

// Additional checks for flows with code.
if ar.Flow == oidc.FlowCode || ar.Flow == oidc.FlowHybrid {
switch ar.CodeChallengeMethod {
case "":
// breaks
case oidc.S256CodeChallengeMethod:
// breaks
case oidc.PlainCodeChallengeMethod:
// Plain is discouraged, and thus not supported.
fallthrough
default:
return ar.NewBadRequest(oidc.ErrorOAuth2InvalidRequest, "transform algorithm not supported")
}
}

if _, hasNonePrompt := ar.Prompts[oidc.PromptNone]; hasNonePrompt {
if len(ar.Prompts) > 1 {
// Cannot have other prompts if none is requested.
Expand Down
5 changes: 4 additions & 1 deletion oidc/payload/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ type RequestObjectClaims struct {
RawIDTokenHint string `json:"id_token_hint"`
RawMaxAge string `json:"max_age"`

RawRegistration string `schema:"registration"`
RawRegistration string `json:"registration"`

CodeChallenge string `json:"code_challenge"`
CodeChallengeMethod string `json:"code_challenge_method"`

client *clients.Secured
}
Expand Down
2 changes: 2 additions & 0 deletions oidc/payload/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ type TokenRequest struct {
ClientID string `schema:"client_id"`
ClientSecret string `schema:"client_secret"`

CodeVerifier string `schema:"code_verifier"`

RedirectURI *url.URL `schema:"-"`
RefreshToken *jwt.Token `schema:"-"`
Scopes map[string]bool `schema:"-"`
Expand Down
8 changes: 8 additions & 0 deletions oidc/provider/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,14 @@ func (p *Provider) TokenHandler(rw http.ResponseWriter, req *http.Request) {
goto done
}

// Validate code challenge according to https://tools.ietf.org/html/rfc7636#section-4.6
if tr.CodeVerifier != "" || ar.CodeChallenge != "" {
if codeVerifierErr := oidc.ValidateCodeChallenge(ar.CodeChallenge, ar.CodeChallengeMethod, tr.CodeVerifier); codeVerifierErr != nil {
err = oidc.NewOAuth2Error(oidc.ErrorOAuth2InvalidGrant, codeVerifierErr.Error())
goto done
}
}

case oidc.GrantTypeRefreshToken:
if tr.RefreshToken == nil {
err = oidc.NewOAuth2Error(oidc.ErrorOAuth2InvalidGrant, "missing refresh_token")
Expand Down

0 comments on commit f8c1f4a

Please sign in to comment.