400 Bad Request coming back when trying to exchange an auth code for an access token

I’m trying to replicate a PKCE solution into ASP.NET. This works in pure JS as per this article

My ASP.NET solution is getting 400 bad request at the webclient call

<%@ Page Language="vb" AutoEventWireup="true" CodeBehind="index.aspx.vb" EnableSessionState="True" Inherits="ASP_OKTA.index" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form runat="server">
        <div class="content">
            <asp:Button ID="start" runat="server" Text="Click to Sign In" />

            <div id="token" class="hidden">
                <h2>Access Token</h2>
                <div id="access_token" class="code" runat="server"></div>
            </div>
            <div id="error" runat="server">
                <h2>Error</h2>
                <div id="error_details" class="code"></div>
            </div>
        </div>
    </form>
</body>
</html>

Public Class index
    Inherits System.Web.UI.Page

    Private Class ConfigClass
        Public client_id As String
        Public redirect_uri As String
        Public authorization_endpoint As String
        Public token_endpoint As String
        Public requested_scopes As String
    End Class

    Private config As New ConfigClass With {
            .client_id = "xxx",
            .redirect_uri = "https://localhost:44348/index.aspx",
            .authorization_endpoint = "https://dev-xxx.okta.com/oauth2/default/v1/authorize",
            .token_endpoint = "https://dev-xxx.okta.com/oauth2/default/v1/token", 
            .requested_scopes = "openid"
        }


    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load

        Dim querystring As NameValueCollection = Request.QueryString

        '   Check if the server returned an error string
        Dim serror As String = querystring("error")

        If serror <> "" Then
            [error].InnerText = serror & " description:" & querystring("error_description")
        End If

        '   If the server returned an authorization code, attempt to exchange it for an access token
        Dim scode As String = querystring("code")


        If scode <> "" Then

            If Session("pkce_state") <> querystring("state") Then
                [error].InnerText = "Invalid state"
            Else
                '   Exchange the authorization code for an access token

                Using wb As New System.Net.WebClient()

                    wb.Headers.Add("Content-Type", "application/x-www-form-urlencoded")

                    Dim Data As New NameValueCollection()
                    Data("grant_type") = "authorization_code"
                    Data("code") = scode
                    Data("client_id") = config.client_id
                    Data("redirect_uri") = config.redirect_uri
                    Data("code_verifier") = Session("pkce_code_verifier")

                    Try
''' ------------- THE FOLLOWING LINE PRODUCES A 400 ERROR ------------
                        Dim res As Byte() = wb.UploadValues(config.token_endpoint, "POST", Data)

                        Dim s As String = Encoding.UTF8.GetString(res)

                        access_token.InnerText = s

                    Catch ex As Exception

                        [error].InnerText = ex.Message

                    End Try

                End Using

            End If

        End If

    End Sub

    Private Sub start_ServerClick(sender As Object, e As EventArgs) Handles start.Click

        '    Create And store a random "state" value
        Dim state As String = GenerateRandomString(28)
        Session("pkce_state") = state

        '    Create And store a New PKCE code_verifier (the plaintext random secret)
        Dim code_verifier As String = GenerateRandomString(28)
        'Dim code_verifier As String = "ksjf836sNHGFtsg5KJiu23dsligt"       ' // produces "XyRe8raF0FjNaowVNTjAR0GBcqJ4efyRJN_IAtqRx8g"

        Session("pkce_code_verifier") = code_verifier

        '    Hash And base64-urlencode the secret to use as the challenge
        Dim code_challenge As String = pkceChallengeFromVerifier(code_verifier)

        Dim x As Integer = 1

        '    Build the authorization URL
        Dim url As String = config.authorization_endpoint &
                "?response_type=code" &
                "&client_id=" & encodeURIComponent(config.client_id) &
                "&state=" & encodeURIComponent(state) &
                "&scope=" & encodeURIComponent(config.requested_scopes) &
                "&redirect_uri=" & encodeURIComponent(config.redirect_uri) &
                "&code_challenge=" & encodeURIComponent(code_challenge) &
                "&code_challenge_method=S256"


        '    Redirect to the authorization server
        Response.Redirect(url)


    End Sub

    Private Function encodeURIComponent(s As String) As String
        Return Uri.EscapeDataString(s)
    End Function

    Private Function GenerateRandomString(length As Integer)

        Const valid As String = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
        Dim res As StringBuilder = New StringBuilder()

        Using rng = New System.Security.Cryptography.RNGCryptoServiceProvider
            Dim uintBuffer(length) As Byte

            While Math.Max(System.Threading.Interlocked.Decrement(length), length + 1) > 0
                rng.GetBytes(uintBuffer)
                Dim num As UInteger = BitConverter.ToUInt32(uintBuffer, 0)
                res.Append(valid(CInt((num Mod CUInt(valid.Length)))))
            End While
        End Using

        Return res.ToString()


    End Function

    Private Function pkceChallengeFromVerifier(v As String) As String
        Dim hashed() As Byte = sha256(v)
        Return base64urlencode(hashed)
    End Function

    '   Calculate the SHA256 hash of the input text. 
    Function sha256(plain) As Byte()

        Dim data() As Byte = Encoding.UTF8.GetBytes(plain)
        Using s As System.Security.Cryptography.SHA256 = System.Security.Cryptography.SHA256.Create()
            Dim hash() As Byte = s.ComputeHash(data)
            Return hash
        End Using

    End Function

    Private Function base64urlencode(b() As Byte) As String
        '   https://stackoverflow.com/questions/26353710/how-to-achieve-base64-url-safe-encoding-in-c - also covers inverse function
        Return System.Convert.ToBase64String(b).TrimEnd("="c).Replace("+", "-").Replace("/", "_")
    End Function

End Class

I’ve been stuck on this for a whole day now and don’t know what else to try. I’ve tried posting a querystring instead of a NV collection, no difference. I also tried stuffing the values into a html form and submitting that - same problem. Yet as I said the code works perfecting in javascript.

I was able to get more information by reading the response json. However I don’t know how to fix this, it’s definitely using the same hash encoding as the javascript version which works every time

The remote server returned an error: (400) Bad Request. response:{“error”:“invalid_grant”,“error_description”:“PKCE verification failed.”}

Finally fixed it. I found that the state and code verifier had to be longer than 28 characters, when I went to 56 characters it worked.

So replace the two occurrences of 28 with 56 above and the code given will work as a simple solution to end to end okta pkce authentication in raw ASP.NET

Another example of end to end auth in pure javascript was posted today here: Authenticate with pure restful API calls - #3 by islwyn10

1 Like

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