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.