Skip to content

Commit

Permalink
Add PKCE support
Browse files Browse the repository at this point in the history
  • Loading branch information
liggitt committed Sep 10, 2016
1 parent f7d1950 commit b657029
Show file tree
Hide file tree
Showing 7 changed files with 356 additions and 0 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ Golang OAuth2 server library
OSIN is an OAuth2 server library for the Go language, as specified at
http://tools.ietf.org/html/rfc6749 and http://tools.ietf.org/html/draft-ietf-oauth-v2-10.

It also includes support for PKCE, as specified at https://tools.ietf.org/html/rfc7636,
which increases security for code-exchange flows for public OAuth clients.

Using it, you can build your own OAuth2 authentication service.

The library implements the majority of the specification, like authorization and token endpoints, and authorization code, implicit, resource owner and client credentials grant types.
Expand Down
34 changes: 34 additions & 0 deletions access.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package osin

import (
"crypto/sha256"
"encoding/base64"
"errors"
"net/http"
"strings"
Expand Down Expand Up @@ -50,6 +52,9 @@ type AccessRequest struct {

// HttpRequest *http.Request for special use
HttpRequest *http.Request

// Optional code_verifier as described in rfc7636
CodeVerifier string
}

// AccessData represents an access grant (tokens, expiration, client, etc)
Expand Down Expand Up @@ -158,6 +163,7 @@ func (s *Server) handleAuthorizationCodeRequest(w *Response, r *http.Request) *A
ret := &AccessRequest{
Type: AUTHORIZATION_CODE,
Code: r.Form.Get("code"),
CodeVerifier: r.Form.Get("code_verifier"),
RedirectUri: r.Form.Get("redirect_uri"),
GenerateRefresh: true,
Expiration: s.Config.AccessExpiration,
Expand Down Expand Up @@ -221,6 +227,34 @@ func (s *Server) handleAuthorizationCodeRequest(w *Response, r *http.Request) *A
return nil
}

// Verify PKCE, if present in the authorization data
if len(ret.AuthorizeData.CodeChallenge) > 0 {
// https://tools.ietf.org/html/rfc7636#section-4.1
if matched := pkceMatcher.MatchString(ret.CodeVerifier); !matched {
w.SetError(E_INVALID_REQUEST, "code_verifier invalid (rfc7636)")
w.InternalError = errors.New("invalid format")
return nil
}

// https: //tools.ietf.org/html/rfc7636#section-4.6
codeVerifier := ""
switch ret.AuthorizeData.CodeChallengeMethod {
case "", PKCE_PLAIN:
codeVerifier = ret.CodeVerifier
case PKCE_S256:
hash := sha256.Sum256([]byte(ret.CodeVerifier))
codeVerifier = base64.RawURLEncoding.EncodeToString(hash[:])
default:
w.SetError(E_INVALID_REQUEST, "code_challenge_method transform algorithm not supported (rfc7636)")
return nil
}
if codeVerifier != ret.AuthorizeData.CodeChallenge {
w.SetError(E_INVALID_GRANT, "code_verifier invalid (rfc7636)")
w.InternalError = errors.New("failed comparison with code_challenge")
return nil
}
}

// set rest of data
ret.Scope = ret.AuthorizeData.Scope
ret.UserData = ret.AuthorizeData.UserData
Expand Down
91 changes: 91 additions & 0 deletions access_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"net/http"
"net/url"
"testing"
"time"
)

func TestAccessAuthorizationCode(t *testing.T) {
Expand Down Expand Up @@ -313,3 +314,93 @@ func TestGetClientSecretMatcher(t *testing.T) {
}
}
}

func TestAccessAuthorizationCodePKCE(t *testing.T) {
testcases := map[string]struct {
Challenge string
ChallengeMethod string
Verifier string
ExpectedError string
}{
"good, plain": {
Challenge: "12345678901234567890123456789012345678901234567890",
Verifier: "12345678901234567890123456789012345678901234567890",
},
"bad, plain": {
Challenge: "12345678901234567890123456789012345678901234567890",
Verifier: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
ExpectedError: "invalid_grant",
},
"good, S256": {
Challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
ChallengeMethod: "S256",
Verifier: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
},
"bad, S256": {
Challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
ChallengeMethod: "S256",
Verifier: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
ExpectedError: "invalid_grant",
},
"missing from storage": {
Challenge: "",
ChallengeMethod: "",
Verifier: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
},
}

for k, test := range testcases {
testStorage := NewTestingStorage()
sconfig := NewServerConfig()
sconfig.AllowedAccessTypes = AllowedAccessType{AUTHORIZATION_CODE}
server := NewServer(sconfig, testStorage)
server.AccessTokenGen = &TestingAccessTokenGen{}
server.Storage.SaveAuthorize(&AuthorizeData{
Client: testStorage.clients["public-client"],
Code: "pkce-code",
ExpiresIn: 3600,
CreatedAt: time.Now(),
RedirectUri: "http://localhost:14000/appauth",
CodeChallenge: test.Challenge,
CodeChallengeMethod: test.ChallengeMethod,
})
resp := server.NewResponse()

req, err := http.NewRequest("POST", "http://localhost:14000/appauth", nil)
if err != nil {
t.Fatal(err)
}

req.SetBasicAuth("public-client", "")

req.Form = make(url.Values)
req.Form.Set("grant_type", string(AUTHORIZATION_CODE))
req.Form.Set("code", "pkce-code")
req.Form.Set("state", "a")
req.Form.Set("code_verifier", test.Verifier)
req.PostForm = make(url.Values)

if ar := server.HandleAccessRequest(resp, req); ar != nil {
ar.Authorized = true
server.FinishAccessRequest(resp, req, ar)
}

if resp.IsError {
if test.ExpectedError == "" || test.ExpectedError != resp.ErrorId {
t.Errorf("%s: unexpected error: %v, %v", k, resp.ErrorId, resp.StatusText)
continue
}
}
if test.ExpectedError == "" {
if resp.Type != DATA {
t.Fatalf("%s: Response should be data", k)
}
if d := resp.Output["access_token"]; d != "1" {
t.Fatalf("%s: Unexpected access token: %s", k, d)
}
if d := resp.Output["refresh_token"]; d != "r1" {
t.Fatalf("%s: Unexpected refresh token: %s", k, d)
}
}
}
}
51 changes: 51 additions & 0 deletions authorize.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package osin
import (
"net/http"
"net/url"
"regexp"
"time"
)

Expand All @@ -12,6 +13,13 @@ type AuthorizeRequestType string
const (
CODE AuthorizeRequestType = "code"
TOKEN AuthorizeRequestType = "token"

PKCE_PLAIN = "plain"
PKCE_S256 = "S256"
)

var (
pkceMatcher = regexp.MustCompile("^[a-zA-Z0-9~._-]{43,128}$")
)

// Authorize request information
Expand All @@ -34,6 +42,11 @@ type AuthorizeRequest struct {

// HttpRequest *http.Request for special use
HttpRequest *http.Request

// Optional code_challenge as described in rfc7636
CodeChallenge string
// Optional code_challenge_method as described in rfc7636
CodeChallengeMethod string
}

// Authorization data
Expand Down Expand Up @@ -61,6 +74,11 @@ type AuthorizeData struct {

// Data to be passed to storage. Not used by the library.
UserData interface{}

// Optional code_challenge as described in rfc7636
CodeChallenge string
// Optional code_challenge_method as described in rfc7636
CodeChallengeMethod string
}

// IsExpired is true if authorization expired
Expand Down Expand Up @@ -140,6 +158,36 @@ func (s *Server) HandleAuthorizeRequest(w *Response, r *http.Request) *Authorize
case CODE:
ret.Type = CODE
ret.Expiration = s.Config.AuthorizationExpiration

// Optional PKCE support (https://tools.ietf.org/html/rfc7636)
if codeChallenge := r.Form.Get("code_challenge"); len(codeChallenge) == 0 {
if s.Config.RequirePKCEForPublicClients && CheckClientSecret(ret.Client, "") {
// https://tools.ietf.org/html/rfc7636#section-4.4.1
w.SetErrorState(E_INVALID_REQUEST, "code_challenge (rfc7636) required for public clients", ret.State)
return nil
}
} else {
codeChallengeMethod := r.Form.Get("code_challenge_method")
// allowed values are "plain" (default) and "S256", per https://tools.ietf.org/html/rfc7636#section-4.3
if len(codeChallengeMethod) == 0 {
codeChallengeMethod = PKCE_PLAIN
}
if codeChallengeMethod != PKCE_PLAIN && codeChallengeMethod != PKCE_S256 {
// https://tools.ietf.org/html/rfc7636#section-4.4.1
w.SetErrorState(E_INVALID_REQUEST, "code_challenge_method transform algorithm not supported (rfc7636)", ret.State)
return nil
}

// https://tools.ietf.org/html/rfc7636#section-4.2
if matched := pkceMatcher.MatchString(codeChallenge); !matched {
w.SetErrorState(E_INVALID_REQUEST, "code_challenge invalid (rfc7636)", ret.State)
return nil
}

ret.CodeChallenge = codeChallenge
ret.CodeChallengeMethod = codeChallengeMethod
}

case TOKEN:
ret.Type = TOKEN
ret.Expiration = s.Config.AccessExpiration
Expand Down Expand Up @@ -191,6 +239,9 @@ func (s *Server) FinishAuthorizeRequest(w *Response, r *http.Request, ar *Author
State: ar.State,
Scope: ar.Scope,
UserData: ar.UserData,
// Optional PKCE challenge
CodeChallenge: ar.CodeChallenge,
CodeChallengeMethod: ar.CodeChallengeMethod,
}

// generate token code
Expand Down
Loading

0 comments on commit b657029

Please sign in to comment.