Skip to main content

Generating an Authorization Code with PKCE

Explanation

The following sample shows the same scenario as the Generating an Authorization Code example, but with a public OAuth Client. In this case no client secret is provided, but instead PKCE is used to verify the requests. The PKCE code challenge that is generated is then required across both the authorize request and the later exchange, see later code samples for how to include the code challenge when making the exchange.

package main

import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"log"
"net/http"
"sync"

cv "github.com/nirasan/go-oauth-pkce-code-verifier"
"golang.org/x/oauth2"
)

const (
ClientID = "client-id"
RedirectUrl = "http://localhost:8080/redirect"

PKCEMethod = "S256" // The algorithm we're using for PKCE.
)

type AuthorizationCode struct {
Code string `json:"code"`
}

func generateState(length int) (string, error) {
state := make([]byte, length)
if _, err := rand.Read(state); err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(state), nil
}

func generatePKCECodeChallenge() (string, string, error) {
codeVerifier, err := cv.CreateCodeVerifier()
if err != nil {
return "", "", err
}
return codeVerifier.CodeChallengeS256(), codeVerifier.String(), nil
}

func main() {
// First we set up our OAuth2 configuration.
//
// We include the ClientID and ClientSecret from our OAuth Client, we
// specify the Authorize and Token URLs, and we set up the redirect URL.
// The values for these fields are set up when you create the OAuth Client
// or are provided in the documentation.
//
// Finally, we specify the Scopes we want to return. The Scopes specified
// here mean we return all permissions available to the user. For more
// information on what this means please view the IMS Permissions and Scopes
// guide.
config := oauth2.Config{
ClientID: ClientID,
Endpoint: oauth2.Endpoint{
AuthURL: "https://platform-auth.improbable.io/auth/v1/authorize",
TokenURL: "https://platform-auth.improbable.io/auth/v1/token",
},
RedirectURL: RedirectUrl,
Scopes: []string{"[*]:*"}, // Replace this with openid for an ID Token
}

// We generate state for our request. Each authorize request should have a
// unique and unguessable state, and it is used to verify the response we
// receive at our redirect URL is a genuine response from our authorize
// request. Normally, you would generate this uniquely for every request -
// but this is just an example.
state, err := generateState(32)
if err != nil {
log.Fatalf("could not generate state: %v", err)
}

// The verifier is used in the exchange step later, so isn't referenced
// further in this code sample. The challenge, however, is used in the
// authorize request and allows the auth service to verify that the
// authorize request and the exchange were made by the same entity.
// challenge, verifier, err := generatePKCECodeChallenge()
challenge, _, err := generatePKCECodeChallenge()
if err != nil {
log.Fatalf("could not generate PKCE challenge: %v", err)
}

// In this example our server will only handle a single sign in request
// before exiting. These variables are only used to handle safely shutting
// down a server.
srv := http.Server{Addr: ":8080"}
wg := &sync.WaitGroup{}
wg.Add(1)

http.HandleFunc(
"/signin",
func(writer http.ResponseWriter, request *http.Request) {
// We redirect signin requests to the authorize endpoint of the
// Resource Owner.

codeChallenge := oauth2.SetAuthURLParam(
"code_challenge",
challenge)
codeChallengeMethod := oauth2.SetAuthURLParam(
"code_challenge_method",
PKCEMethod)

http.Redirect(
writer,
request,
config.AuthCodeURL(state, codeChallenge, codeChallengeMethod),
http.StatusTemporaryRedirect)
})

http.HandleFunc(
"/redirect",
func(writer http.ResponseWriter, request *http.Request) {
// We listen for requests on our redirect endpoint, verify that any
// requests that arrive are expected, and extract the authorization
// code from these requests.

if request.URL.Query().Get("state") != state {
// If the returned state does not match our outgoing state then
// this redirect was not generated by us, so we fail the
// request.
http.Error(writer, err.Error(), http.StatusInternalServerError)
return
}

// Now that we've verified the request was legitimate we can pull
// the Authorization Code from the query string.
authorizationCode := request.URL.Query().Get("code")

// Format the Authorization Code and pass it to the writer to
// display to the user.
formatForScreen(writer, authorizationCode)

// Now we've handled exactly one request, let's shut the server
// down. The setup here isn't what you would expect in production,
// but given the way we are generating the state once and only once
// we shouldn't allow more than one request per server execution.
go func() {
if err := srv.Shutdown(context.Background()); err != nil {
log.Fatalf("Shutdown(): %v", err)
}
}()
})

// The rest of this function just starts a server on your local machine and
// waits for exactly one request at the redirect endpoint before exiting.

// Start the server.
go func() {
defer wg.Done() // Let the WaitGroup know the server has exited.

if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("ListenAndServe(): %v", err)
}
}()

wg.Wait() // Wait for the server to shutdown before exiting main.
}

// Normally, you wouldn't return the code for display anywhere but just use it
// to immediately generate an Access Token. See the "Exchanging an Authorization
// Code for an Access Token with PKCE" code sample on how to do this.
func formatForScreen(writer http.ResponseWriter, authorizationCode string) {
r, err := json.Marshal(AuthorizationCode{Code: authorizationCode})
if err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError)
return
}

if _, err := writer.Write(r); err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError)
return
}
writer.WriteHeader(http.StatusOK)
writer.Header().Set("Content-Type", "application/json")
}