I have come across several articles about creating public PKCE clients. However, they end up actually using the client secret in their configurations which is not what I would like to do. Therefore, the purpose of this demo is to show that a public client can be created without using a client-secret.
This demo will create a Public PKCE client using NextJS with NextAuth api.
For a official documentations on PKCE OAuth2 flow refer to this.
This is a short demo which will use my authorization server, an OAuth2 server. I will then show the Nextjs project that uses the NextAuth library to login and get user information.
First we need to create a Public PKCE client on the Authorization server. The following image show my pkce client configured on my authorization server:
.
In my above client configuration the client secret is actually a empty space but it is encrypted which I am not going to use.
The client authentication method is done using client_secret_basic
where it means that the client id is sent in the http header similar to basic authentication http header. We also check NONE
because we are using PKCE and we are going to be sending a code_verifier
and a code_challenge
pairs in our exchanges with the authorization server for receiving a token. For more on this NONE
read this.
The grant types I have are AUTHORIZATION_CODE
where we exchange authorization code for getting a token.
I have also requested 3 scopes such as OPENID
, PROFILE
and EMAIL
for displaying user information on the front end.
The redirect uris refers to where to redirect after successful authentication on my authorization server.
We have now created our OAuth2 client. In the next step we create the OAuth2 client in NextJS.
I am using Nextauth for authentication/authorization with my authorization server. I am going to refer to my nextauth project in my github repo as source for this demo.
I will show the user interfaces first because I find that it helps to demonstrate the core concepts easier.
I have deployed the repo on my server and is accessible at nextauth.sonam.cloud:
.
When the user clicks the sign-in
button on the page above, the user will be directed to the authorization sign-in page:
After the user enters their credentials and with successful authentication, they will be directed to authenticated Home
page as shown here:
The logged-in username is printed in the header for an authenticated session. This completes the demo from the UI side.
To achieve this, you have to configure few things and they are done in this [..nextauth].ts file.
First, I am using the pkce-challenge
api to generate a code_challenge
and a code_verifier
with a S256 hashing algorithm as follows:
const pkce = await pkceChallenge(128)
In terms of workflow related to the pkce flow, the code_challenge is sent first in the authorization request when the sign-in button is clicked. Once the user enters their username/password in the sign-in page the code_verifier is sent to receive a token from the authorization server on a successful login.
The following code sets up a custom OAuth2 provider with my authorization server:
export const authOptions: NextAuthOptions = {
providers: [
{
id: "myauth",
name: "SonamCloud",
type: "oauth",
clientId: clientId,
wellKnown: auth_server + "/.well-known/openid-configuration",
In the next block of code I am setting the parameters for authorization request. For this public pkce client I am setting code_challenge: pkce.code_challenge
in authorization request which is sent to the OAuth2 server.
authorization: {
url: auth_server+ "/oauth2/authorize?myvalue=ajksdfkjsdfi",
params: {
scope: "openid email profile",
prompt: 'Select Account',
code_challenge: pkce.code_challenge,
code_challenge_method: "S256",
redirect_uri: host + "/api/auth/callback/myauth"
},
},
Once the user enters their credentials in the login page the application will forward the code verifier in the token request. The following is the token endpoint configuration using Nextauth provider configuration. This async function await makeTokenRequest(context)
is called which will send the token request.
token: {
url: auth_server + "/oauth2/token",
async request(context) {
console.log("code: %s, redirect_uri: %s", context.params.code, context.params.redirect_uri)
console.log("making token request");
const tokens = await makeTokenRequest(context)
console.log('tokens: {}', tokens)
return { tokens }
}
The makenTokenRequest
mentioned above sets the code verifier and is shown below:
async function makeTokenRequest(context: any) {
console.log("params: ",context.params)
console.log('host: ', host, ', nextAuthUrl: ', process.env.NEXTAUTH_URL)
const formData = new URLSearchParams();
formData.append('grant_type', 'authorization_code')
formData.append('code', context.params.code)
formData.append('client_id', clientId)
formData.append('redirect_uri', host+ '/api/auth/callback/myauth')
formData.append('code_verifier', pkce.code_verifier)
That is pretty much what is required to create a public PKCE client that does not use a client secret.
The public client will issue access tokens using my authorization server but will not issue refresh tokens at this time.
The client is recommended to use short lived access tokens for this reason.