How to persist and check the state, nonce in OIDC code flow

Hi guys,

I’m trying to implement the OIDC Code flow for Facebook.
source: https://developers.facebook.com/docs/facebook-login/guides/advanced/oidc-token

This is how it works so far:

Code

The code looks like this (largely inspired form this blog post: https://chrisguitarguy.com/2022/12/07/oauth-pkce-with-go/)

package controllers

var (
  state string
  codeVerifier string
  nonce string
)

var fbOAuthConfig = &oauth2.Config{
	ClientID:     "xxxxxxxx",
	ClientSecret: "xxxxxxxx",
	Scopes:       []string{"openid"},
        RedirectURL: "", // received by the authEndpointHandler below
	Endpoint: oauth2.Endpoint{
		AuthURL:  "https://www.facebook.com/v17.0/dialog/oauth",
		TokenURL: "https://graph.facebook.com/v17.0/oauth/access_token",
	},
}

// 1) after the user clicks the Facebook btn on the front, he is redirected to this endpoint 
func authEndpointHandler(w http.ResponseWriter, req *http.Request){
  // assign the redirect URL to FB OAuth Config
  FBOAuthConfig.RedirectURL = req.URL.Query().Get("redirect_url")

  // generate authorization URL
  ...

  // save nonce, state and codeVerifier
   nonce = authURL.Nonce
   state = authURL.State
   codeVerifier = authURL.CodeVerifier

   // redirect to the authorization URL
   http.Redirect(w, req, authURL.URL, http.StatusFound)
}

// 2) once the user give approval, Facebook sends a request to this handler
func responseEndpointHandler(w http.ResponseWriter, req *http.Request){
        // get the state
        requestState := query.Get("state")
	
	// from the doc: ConstantTimeCompare returns 1 if the two slices, x and y, have equal contents and 0 otherwise.
	if subtle.ConstantTimeCompare([]byte(requestState), []byte(state)) == 0 {
		textResponse(w, http.StatusBadRequest, "Invalid State")
		return
	}

	// get the code
	code := query.Get("code")
	if code == "" {
		textResponse(w, http.StatusBadRequest, "Missing Code")
		return
	}

        // 3) exchange the code received by Facebook with an id_token
	// from the doc: Exchange converts an authorization code into a token.
	tokenResp, err := fbOAuthConfig.Exchange(
		req.Context(),
		code,		oauth2.SetAuthURLParam("code_verifier", codeVerifier),
		oauth2.SetAuthURLParam("redirect_uri", "https://front.app.com/signin/success)",
	)
	if err != nil {
		textResponse(w, http.StatusInternalServerError, err.Error())
		return
	}
       
       // extract the id token from the response and validate it
       idToken:= tokenResp.Extra("id_token")
      Verify(idToken)
      ...
      // redirect to the required URL
      http.Redirect(w, req, fbOAuthConfig.RedirectURL, http.StatusFound)
}

Problem

When I make a test, I works. But there’s something obvious I’m missing …

My backend API will probably receive multiple request at the same time, and today, I store the nounce, state and code_verifier as package variables so if the authEndpointHandler receives a request before responseEndpointHandler finishes, then the nounce, state and code_verifier values would be overiden by the incoming request.

Idea

I thought about creating a map like below in which the key would be the state.

var requestStates = map[string]struct {
	CodeVerifier string
	Nonce        string
}{}

Then I would just have to:

  • ensure not to generate twice the same state in a short period of time.
  • remove each item once both handlers are finished to avoid collision between keys.

With this idea, the responseEndpointHandler func would look something like this:

package controller
// ...
const REDIRECT_URI = "https://localhost:3000/auth/facebook/access-token"

var FBOAuthConfig = &oauth2.Config{
	ClientID:     "xxxxx",
	ClientSecret: "xxxxx",
	Scopes:       []string{"openid",  "email", "public_profile"},
	RedirectURL:  "", // retrieved as a query string parameter
	Endpoint: oauth2.Endpoint{
		AuthURL:  "https://www.facebook.com/v17.0/dialog/oauth",
		TokenURL: "https://graph.facebook.com/v17.0/oauth/access_token",
	},
}
var states = map[string]struct {
	CodeVerifier       string
	Nonce              string
	BrowserRedirectURL string
}{}

// 1) STEP 1: handle GET /auth/facebook/signin?redirect_url=https://localhost/signin/success
func (ctrl *controller) RedirectToAuthURL(w http.ResponseWriter, req *http.Request) {
	FBOAuthConfig.RedirectURL = req.URL.Query().Get("redirect_url")

	authURL, err := ctrl.getAuthorizationURL(FBOAuthConfig)
	if err != nil {
		helpers.ErrorJSON(w, http.StatusBadRequest, "bad request")
		return
	}

	// save the nonce and codeVerifier and make it match to the created state
	// 1 request = 1 state = 1 nonce + 1 code verifier + 1 BrowserRedirectURL
	states[authURL.State] = struct {
		CodeVerifier       string
		Nonce              string
		BrowserRedirectURL string
	}{
		authURL.CodeVerifier,
		authURL.Nonce,
		req.URL.Query().Get("redirect_url"),
	}

	http.Redirect(w, req, authURL.URL, http.StatusFound)
}

// 2) STEP 2: handle GET /auth/facebook/access-token?code=XXXXXXXXXXXX&state=XXXXXXXXXXXX
func (ctrl *controller) HandleResponse(w http.ResponseWriter, req *http.Request) {
	requestState := req.URL.Query().Get("state")
	if requestState == "" {
		helpers.ErrorJSON(w, http.StatusBadRequest, "request state can't be empty")
		return
	}

	// from doc: ConstantTimeCompare returns 1 if the two slices, x and y, have equal contents and 0 otherwise.
	// Verify state and prevent timing attacks on state
	// if subtle.ConstantTimeCompare([]byte(requestState), []byte(state)) == 0 {
	// 	textResponse(w, http.StatusBadRequest, "Invalid State")
	// 	return
	// }

	// check if state exists
	state, ok := states[requestState]
	if !ok {
		helpers.ErrorJSON(w, http.StatusBadRequest, "invalid sequence, state doesn't exist")
		return
	}

	// get the code
	code := query.Get("code")
	if code == "" {
		helpers.ErrorJSON(w, http.StatusBadRequest, "missing code")
		return
	}

	// STEP 3) Exchange converts an authorization code into a token.
	tokenResp, err := FBOAuthConfig.Exchange(
		req.Context(),
		code,
		oauth2.SetAuthURLParam("code_verifier", state.CodeVerifier),
		oauth2.SetAuthURLParam("redirect_uri", REDIRECT_URI),
	)
	if err != nil {
		helpers.ErrorJSON(w, http.StatusBadRequest, "exchange code error: "+ err)
		return
	}

	//  verify ID Token
	claims, err := verifyIDToken(tokenResp.Extra("id_token").(string))
	if err != nil {
		helpers.ErrorJSON(w, http.StatusBadRequest, "invalid id token: "+ err)
		return
	}

	// verify nonce
	if claims["nonce"] != state.Nonce{
		helpers.ErrorJSON(w, http.StatusBadRequest, "invalid id token: invalid nonce")
	}
	
	// set a cookie etc.
	// ...

	// END) redirect to front end success URL
	http.Redirect(w, req, state.RedirectURL, http.StatusFound)
}

func (ctrl *controller) getAuthorizationURL(config *oauth2.Config) (*oauthdom.AuthURL, resterrors.RestErr) {
	// create code verifier
	codeVerifier, err := randomBytesInHex(32) // 64 character string here
	if err != nil {
		return nil, restErr
	}

	// create code challenge
	sha2 := sha256.New()
	io.WriteString(sha2, codeVerifier)
	codeChallenge := base64.RawURLEncoding.EncodeToString(sha2.Sum(nil))

	// create state
	state, err := randomBytesInHex(24)
	if err != nil {
		return nil, err
	}

	// create nonce
	nonce, err := randomBytesInHex(12)
	if err != nil {
		return nil, err
	}

	// create auth URL
	// from doc: oauth2.SetAuthURLParam append the appropriate query string values to the authorization url.
	authUrl := config.AuthCodeURL(
		state,
		oauth2.SetAuthURLParam("redirect_uri", REDIRECT_URI),
		oauth2.SetAuthURLParam("scope", "openid, email, public_profile"),
		oauth2.SetAuthURLParam("redirect_type", "code"),
		oauth2.SetAuthURLParam("code_challenge", codeChallenge), // should be stored in sessions or HTTP only cookies, in memory, etc. WITH THE URL
		oauth2.SetAuthURLParam("code_challenge_method", "S256"), // should be stored in sessions or HTTP only cookies, in memory, etc. WITH THE URL
		oauth2.SetAuthURLParam("nonce", nonce),
	)

	return &oauthdom.AuthURL{
		URL:          authUrl,
		State:        state,
		CodeVerifier: codeVerifier,
		Nonce:        nonce,
	}, nil
}

// Used to Generate a code verifier and a state
func randomBytesInHex(count int) (string, error) {
	buf := make([]byte, count)
	_, err := io.ReadFull(rand.Reader, buf)
	if err != nil {
		return "", fmt.Errorf("Could not generate %d random bytes: %v", count, err)
	}

	return hex.EncodeToString(buf), nil
}

I really don’t know if I’m completely off so any comment/advice is welcome :slight_smile:

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.