Skip to main content

Generating an Authorization Code

Explanation

The following code sample starts an HTTP server that has two endpoints, a signin and a redirect endpoint. When a user visits the signin endpoint via a browser it redirects the user to the IMS Platform Auth authorization endpoint where they can sign in. On a successful sign in the IMS Platform Auth service sends the user back to the redirect endpoint. The redirect endpoint then extracts the Authorization Code and displays it to the user.

The server started by the code sample only allows a single sign in request per execution. This is because we’re not generating the unique state correctly (as it’s just an example). In production every authorize request should have a unique state generated and supplied alongside it. The authorization server then includes that state when the request makes its way back to the user, and the local server can verify that it’s a valid response to the sign in request. The code sample below generates a single global state for this, which is not correct for a production system. We have done this in the example to save us having to handle writing cookies or saving any kind of backend data.

The following example assumes the existence of a web application or server. It is possible to retrieve Authorization Codes from CLI or desktop applications as long as they can open a browser. Applications can open a browser that points to the authorization endpoint, while opening a localhost endpoint to handle the redirect. Upon receiving the redirect the application can extract the Authorization Code and close the redirect endpoint.

package main

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

"golang.org/x/oauth2"
)

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

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 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,
ClientSecret: ClientSecret,
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)
}

// 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.

http.Redirect(
writer,
request,
config.AuthCodeURL(state),
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" 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")
}