mirror of
https://github.com/ysoftdevs/oauth-playground-client.git
synced 2026-01-11 22:41:29 +01:00
PKCE implementation
This commit is contained in:
@@ -93,11 +93,18 @@ body {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
width: 70%;
|
||||
margin: 0 auto;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.circle-3 {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.circle-4 {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.circle {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<div class="container">
|
||||
<div class="section">
|
||||
<h3 class="header centered">Authorization Code Flow</h3>
|
||||
<div class="circle-container">
|
||||
<div class="circle-container circle-3">
|
||||
<div class="circle">
|
||||
1
|
||||
</div>
|
||||
@@ -119,17 +119,8 @@
|
||||
<footer class="page-footer"></footer>
|
||||
<script src="../js/load-layout.js"></script>
|
||||
<script>
|
||||
function setCookie(name, value, minutes) {
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + (minutes * 60 * 1000));
|
||||
const expires = "; expires=" + date.toUTCString();
|
||||
document.cookie = name + "=" + value + expires + "; path=/";
|
||||
}
|
||||
|
||||
function generateSessionState () {
|
||||
const sessionState = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
||||
setCookie("sessionState", sessionState, 15);
|
||||
return sessionState;
|
||||
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
||||
}
|
||||
|
||||
function constructRequestUrl () {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<div class="container">
|
||||
<div class="section">
|
||||
<h3 class="header centered">Authorization Code Flow</h3>
|
||||
<div class="circle-container">
|
||||
<div class="circle-container circle-3">
|
||||
<div class="circle">
|
||||
1
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<div class="container">
|
||||
<div class="section">
|
||||
<h3 class="header centered">Authorization Code Flow</h3>
|
||||
<div class="circle-container">
|
||||
<div class="circle-container circle-3">
|
||||
<div class="circle">
|
||||
1
|
||||
</div>
|
||||
|
||||
141
src/flow/pkce-1.html
Normal file
141
src/flow/pkce-1.html
Normal file
@@ -0,0 +1,141 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OAuth 2.0 Playground - PKCE Flow (1/4)</title>
|
||||
<link rel="icon" href="../favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
|
||||
<link type="text/css" rel="stylesheet" href="../css/style.css" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;600;700&display=swap" />
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header id="page-header"></header>
|
||||
<main>
|
||||
<div class="container">
|
||||
<div class="section">
|
||||
<h3 class="header centered">PKCE Flow</h3>
|
||||
<div class="circle-container circle-4">
|
||||
<div class="circle">
|
||||
1
|
||||
</div>
|
||||
<div class="line line-inactive"></div>
|
||||
<div class="circle circle-inactive">
|
||||
2
|
||||
</div>
|
||||
<div class="line line-inactive"></div>
|
||||
<div class="circle circle-inactive">
|
||||
3
|
||||
</div>
|
||||
<div class="line line-inactive"></div>
|
||||
<div class="circle circle-inactive">
|
||||
4
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col s3 circle-text">
|
||||
Create a secret code verifier and code challenge
|
||||
</div>
|
||||
<div class="col s3 circle-text">
|
||||
Build the authorization URL and redirect the user to the authorization server
|
||||
</div>
|
||||
<div class="col s3 circle-text">
|
||||
After the user is redirected back to the client, verify the state
|
||||
</div>
|
||||
<div class="col s3 circle-text">
|
||||
Exchange the authorization code and code verifier for an access token
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="col s12 m7">
|
||||
<div class="card horizontal">
|
||||
<div class="card-stacked">
|
||||
<div class="card-content">
|
||||
<h6>1. Create a Code Verifier and Challenge</h6>
|
||||
<p>Before we can start the authorization process, we need to create a <b>code verifier</b> and a <b>code challenge</b>. The code verifier is a cryptographically random string that is used to verify the identity of the client. The code challenge is a hashed version of the code verifier, which is sent to the authorization server. The authorization server will then compare the code challenge with the code verifier to verify the identity of the client.</p>
|
||||
</p>
|
||||
<div class="row" style="margin-top: 20px;">
|
||||
<div class="col s3">
|
||||
<div class="waves-effect waves-light btn full-width" onclick="generateCodeVerifier()">Generate Code Verifier</div>
|
||||
</div>
|
||||
<div class="col s9">
|
||||
<input id="codeVerifier" style="height: 30px;" type="text" disabled/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>Now that we habe the code verified, we need to create the code challenge. We do so by hashing the code verifier using the SHA256 algorithm and then encoding it using the URL-safe Base64 encoding.</p>
|
||||
|
||||
<div class="row" style="margin-top: 20px;">
|
||||
<div class="col s3">
|
||||
<div id="code-challenge-btn" class="waves-effect waves-light btn full-width disabled" onclick="generateCodeChallenge()">Generate Code Challenge</div>
|
||||
</div>
|
||||
<div class="col s9">
|
||||
<input id="codeChallenge" style="height: 30px;" type="text" disabled />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>Now that we have the code verifier and code challenge, we can start the authorization process.</p>
|
||||
</p>
|
||||
<div class="row flow-submit-container">
|
||||
<a id="continue-btn" class="waves-effect waves-light btn full-width disabled"
|
||||
href="/flow/pkce-2">Continue</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section centered">
|
||||
<a href="/">[ Take me home ]</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="page-footer"></footer>
|
||||
<script src="../js/load-layout.js"></script>
|
||||
<script src="../js/cookies.js"></script>
|
||||
<script>
|
||||
function generateCodeVerifier() {
|
||||
var verifier = '';
|
||||
var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
|
||||
for (var i = 0; i < 64; i++) {
|
||||
verifier += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
|
||||
$('#codeVerifier').val(verifier);
|
||||
$('#codeChallenge').val("");
|
||||
$('#continue-btn').addClass('disabled');
|
||||
$('#code-challenge-btn').removeClass('disabled');
|
||||
|
||||
setCookie("code_verifier", verifier, 30);
|
||||
}
|
||||
|
||||
async function generateCodeChallenge() {
|
||||
const verifier = $('#codeVerifier').val();
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(verifier);
|
||||
|
||||
const digest = await crypto.subtle.digest('SHA-256', data);
|
||||
|
||||
// Convert hash to Base64 URL encoded string
|
||||
const base64UrlEncoded = btoa(String.fromCharCode(...new Uint8Array(digest)))
|
||||
.replace('+', '-')
|
||||
.replace('/', '_')
|
||||
.replace(/=+$/, '');
|
||||
|
||||
$('#codeChallenge').val(base64UrlEncoded);
|
||||
$('#continue-btn').removeClass('disabled');
|
||||
|
||||
setCookie("code_challenge", base64UrlEncoded, 30);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
190
src/flow/pkce-2.html
Normal file
190
src/flow/pkce-2.html
Normal file
@@ -0,0 +1,190 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OAuth 2.0 Playground - PKCE Flow (2/4)</title>
|
||||
<link rel="icon" href="../favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
|
||||
<link type="text/css" rel="stylesheet" href="../css/style.css" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;600;700&display=swap" />
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header id="page-header"></header>
|
||||
<main>
|
||||
<div class="container">
|
||||
<div class="section">
|
||||
<h3 class="header centered">PKCE Flow</h3>
|
||||
<div class="circle-container circle-4">
|
||||
<div class="circle">
|
||||
1
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="circle">
|
||||
2
|
||||
</div>
|
||||
<div class="line line-inactive"></div>
|
||||
<div class="circle circle-inactive">
|
||||
3
|
||||
</div>
|
||||
<div class="line line-inactive"></div>
|
||||
<div class="circle circle-inactive">
|
||||
4
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col s3 circle-text">
|
||||
Create a secret code verifier and code challenge
|
||||
</div>
|
||||
<div class="col s3 circle-text">
|
||||
Build the authorization URL and redirect the user to the authorization server
|
||||
</div>
|
||||
<div class="col s3 circle-text">
|
||||
After the user is redirected back to the client, verify the state
|
||||
</div>
|
||||
<div class="col s3 circle-text">
|
||||
Exchange the authorization code and code verifier for an access token
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="col s12 m7">
|
||||
<div class="card horizontal">
|
||||
<div class="card-stacked">
|
||||
<div class="card-content">
|
||||
<h6>2. Build the Authorization URL</h6>
|
||||
<p>
|
||||
First we need to build the authorization URL and redirect the user to the authorization server. The URL is constructed as follows:
|
||||
</p>
|
||||
<pre class="code-block"><code id="requestUriExample"></code></pre>
|
||||
<p>Let's break it down...</p>
|
||||
<ul class="collection">
|
||||
<li class="collection-item">
|
||||
<p><b><span id="baseUrl"></span></b>
|
||||
</p>
|
||||
<p>URL of the authorization endpoint on the server. How is this path constructed will
|
||||
differ between OAuth providers (such as Keycloak, Okta, etc.).
|
||||
</p>
|
||||
</li>
|
||||
<li class="collection-item">
|
||||
<p><b><span class="emphasis">response_type</span>=<span id="responseType"></span></b></p>
|
||||
<p>OAuth 2.0 response type. In this case, we are using the Authorization Code flow, so
|
||||
we are requesting the authorization <b>code</b>.</p>
|
||||
</li>
|
||||
<li class="collection-item">
|
||||
<p><b><span class="emphasis">client_id</span>=<span id="clientId"></span></b></p>
|
||||
<p>Client ID of the application. This is a public identifier for the client, and it is
|
||||
used by the authorization server to identify the application
|
||||
when redirecting the user back to the client.</p>
|
||||
</li>
|
||||
<li class="collection-item">
|
||||
<p><b><span class="emphasis">redirect_uri</span>=<span id="redirectUri"></span></b></p>
|
||||
<p>Redirect URI of the client. This is the URL that the authorization server will
|
||||
redirect the user back to after the user has logged in and
|
||||
granted permissions. The redirect URI must match one of the URIs registered for the
|
||||
client ID.</p>
|
||||
</li>
|
||||
<li class="collection-item">
|
||||
<p><b><span class="emphasis">scope</span>=<span id="scope"></span></b></p>
|
||||
<p>Scopes requested by the client. Scopes are used to limit the access of the access
|
||||
token. In this case, we are requesting the <b>offline_access</b> scope,
|
||||
which allows the client to obtain a refresh token.</p>
|
||||
</li>
|
||||
<li class="collection-item">
|
||||
<p><b><span class="emphasis">state</span>=<span id="state"></span></b></p>
|
||||
<p>State parameter. This is an <b>optional parameter</b> that the client can use to maintain
|
||||
state between the request and callback. The authorization
|
||||
server includes this parameter when redirecting the user back to the client,
|
||||
allowing the client to verify that the response is coming from the
|
||||
server and not a malicious third party (<a href="https://owasp.org/www-community/attacks/csrf" target="_blank">CSRF attack</a>).</p>
|
||||
</li>
|
||||
<li class="collection-item">
|
||||
<p><b><span class="emphasis">code_challenge</span>=<span id="codeChallenge"></span></b></p>
|
||||
<p>This is the code challenge we have created in the previous step. This ensures that even if an attacker intercepts the authorization code, they can't exchange it for an access token without the original code verifier.</p>
|
||||
</li>
|
||||
<li class="collection-item">
|
||||
<p><b><span class="emphasis">code_challenge_method</span>=<span id="codeChallengeMethod"></span></b></p>
|
||||
<p>Code Challenge method tells the identity provider how the <b>code_challenge</b> was generated from the original code verifier. <b>S256</b> means that
|
||||
the challenge is a base64url encoding of the SHA-256 hash of the verifier. This is the recommended method for PKCE.</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>All that we now need to do is click the button below and login with our credentials.
|
||||
For the purposes of this
|
||||
playground we already took the liberty to create <b>user</b> with password <b>user</b> for you. After your credentials are successfully verified, you will be redirected back to this playground, to the URL we have specified in the <b>redirect_uri</b> query parameter of the request.</p>
|
||||
<div class="row flow-submit-container">
|
||||
<a id="sendRequestBtn" class="waves-effect waves-light btn full-width"
|
||||
href="#">Authenticate</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section centered">
|
||||
<a href="/">[ Take me home ]</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="page-footer"></footer>
|
||||
<script src="../js/load-layout.js"></script>
|
||||
<script src="../js/cookies.js"></script>
|
||||
<script>
|
||||
function generateSessionState () {
|
||||
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
||||
}
|
||||
|
||||
function constructRequestUrl () {
|
||||
return baseUrl
|
||||
+ "?" + "response_type=" + responseType
|
||||
+ "&" + "client_id=" + clientId
|
||||
+ "&" + "redirect_uri=" + redirectUri
|
||||
+ "&" + "scope=" + scope
|
||||
+ "&" + "state=" + state
|
||||
+ "&" + "code_challenge=" + codeChallenge
|
||||
+ "&" + "code_challenge_method=S256";
|
||||
}
|
||||
|
||||
function fillExample() {
|
||||
const requestExample = baseUrl + "\n"
|
||||
+ " ?response_type=" + responseType + "\n"
|
||||
+ " &client_id=" + clientId + "\n"
|
||||
+ " &redirect_uri=" + redirectUri + "\n"
|
||||
+ " &scope=" + scope + "\n"
|
||||
+ " &state=" + state + "\n"
|
||||
+ " &code_challenge=" + codeChallenge + "\n"
|
||||
+ " &code_challenge_method=S256";
|
||||
|
||||
$("#requestUriExample").text(requestExample);
|
||||
$("#baseUrl").text(baseUrl);
|
||||
$("#responseType").text(responseType);
|
||||
$("#clientId").text(clientId);
|
||||
$("#redirectUri").text(redirectUri);
|
||||
$("#scope").text(scope);
|
||||
$("#state").text(state);
|
||||
$("#codeChallenge").text(codeChallenge);
|
||||
$("#codeChallengeMethod").text("S256");
|
||||
}
|
||||
|
||||
function getRedirectUri() {
|
||||
return window.location.protocol + "//" + window.location.host + "/flow/pkce-3";
|
||||
}
|
||||
|
||||
const baseUrl = "https://sso.rumbuddy.cz/realms/OAuthPlayground/protocol/openid-connect/auth";
|
||||
const responseType = "code";
|
||||
const clientId = "oauth-playground";
|
||||
const redirectUri = getRedirectUri();
|
||||
const scope = "offline_access";
|
||||
const state = generateSessionState();
|
||||
const codeChallenge = getCookie("code_challenge");
|
||||
|
||||
fillExample();
|
||||
$("#sendRequestBtn").attr("href", constructRequestUrl());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
124
src/flow/pkce-3.html
Normal file
124
src/flow/pkce-3.html
Normal file
@@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OAuth 2.0 Playground - PKCE Flow (3/4)</title>
|
||||
<link rel="icon" href="../favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
|
||||
<link type="text/css" rel="stylesheet" href="../css/style.css" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;600;700&display=swap" />
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header id="page-header"></header>
|
||||
<main>
|
||||
<div class="container">
|
||||
<div class="section">
|
||||
<h3 class="header centered">PKCE Flow</h3>
|
||||
<div class="circle-container circle-4">
|
||||
<div class="circle">
|
||||
1
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="circle">
|
||||
2
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="circle">
|
||||
3
|
||||
</div>
|
||||
<div class="line line-inactive"></div>
|
||||
<div class="circle circle-inactive">
|
||||
4
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col s3 circle-text">
|
||||
Create a secret code verifier and code challenge
|
||||
</div>
|
||||
<div class="col s3 circle-text">
|
||||
Build the authorization URL and redirect the user to the authorization server
|
||||
</div>
|
||||
<div class="col s3 circle-text">
|
||||
After the user is redirected back to the client, verify the state
|
||||
</div>
|
||||
<div class="col s3 circle-text">
|
||||
Exchange the authorization code and code verifier for an access token
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="col s12 m7">
|
||||
<div class="card horizontal">
|
||||
<div class="card-stacked">
|
||||
<div class="card-content">
|
||||
<h6>3. Verify the state parameter</h6>
|
||||
<p>You have now been redirected back to the application, to the page that was specified in the <b>redirect-url</b> parameter. In the URL you can notice, that there are addtional query parameters:</p>
|
||||
<pre class="code-block"><code id="queryParams"></code></pre>
|
||||
<p>Let's break it down...</p>
|
||||
<ul class="collection">
|
||||
<li class="collection-item">
|
||||
<p><b><span class="emphasis">state</span>=<span id="state"></span></b></p>
|
||||
<p>The state parameter is an opaque value used by the client to maintain state between the request and the callback.
|
||||
Essentially, it is used to prevent Cross-Site Request Forgery (<a href="https://owasp.org/www-community/attacks/csrf" target="_blank">CSRF</a>) attacks and to ensure the response belongs to the request made by the client.
|
||||
<p>The state value isn't strictly necessary here since the PKCE parameters provide CSRF protection themselves. In practice,
|
||||
if you're sure the OAuth server supports PKCE, you can use the state parameter for application state instead of using it
|
||||
for CSRF protection.</p>
|
||||
</p>
|
||||
</li>
|
||||
<li class="collection-item">
|
||||
<p><b><span class="emphasis">session_state</span>=<span id="sessionState"></span></b></p>
|
||||
<p>The session state parameter is not a core part of the OAuth 2.0 specification, but it is used in OpenID Connect (OIDC)
|
||||
to represent the state of the end user's session at the Authorization Server.
|
||||
The client can use this value to help manage user sessions or to detect when the user's session at the Authorization
|
||||
Server changes (for example, if the user logs out).</p>
|
||||
</li>
|
||||
<li class="collection-item">
|
||||
<p><b><span class="emphasis">code</span>=<span id="code"></span></b></p>
|
||||
<p>The code parameter contains the actual authorization code. This is a temporary code that the client can exchange for an
|
||||
access token (and optionally, a refresh token) by making a back-channel request to the Authorization Server.
|
||||
The format and structure of the code is determined by the Authorization Server. It can be just a random string, or a more complex construction. The exact significance of this structure is specific to the Authorization Server implementation and might include different identifiers or information encoded in
|
||||
the structure.</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>Now we have everything necessary to obtain token for the user. But is the state we have sent equivalent to the one we received back?</p>
|
||||
<div class="row flow-submit-container">
|
||||
<a class="waves-effect waves-light btn full-width"
|
||||
onclick="proceedToNextStep()">States are matching</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section centered">
|
||||
<a href="/">[ Take me home ]</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="page-footer"></footer>
|
||||
<script src="../js/load-layout.js"></script>
|
||||
<script>
|
||||
$("#queryParams").text(window.location.search)
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const code = urlParams.get('code');
|
||||
const state = urlParams.get('state');
|
||||
const sessionState = urlParams.get('session_state');
|
||||
|
||||
$("#state").text(state);
|
||||
$("#sessionState").text(sessionState);
|
||||
$("#code").text(code);
|
||||
|
||||
function proceedToNextStep() {
|
||||
window.location.href = "/flow/pkce-4" + window.location.search;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
241
src/flow/pkce-4.html
Normal file
241
src/flow/pkce-4.html
Normal file
@@ -0,0 +1,241 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OAuth 2.0 Playground - PKCE Flow (4/4)</title>
|
||||
<link rel="icon" href="../favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
|
||||
<link type="text/css" rel="stylesheet" href="../css/style.css" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;600;700&display=swap" />
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header id="page-header"></header>
|
||||
<main>
|
||||
<div class="container">
|
||||
<div class="section">
|
||||
<h3 class="header centered">PKCE Flow</h3>
|
||||
<div class="circle-container circle-4">
|
||||
<div class="circle">
|
||||
1
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="circle">
|
||||
2
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="circle">
|
||||
3
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="circle">
|
||||
4
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col s3 circle-text">
|
||||
Create a secret code verifier and code challenge
|
||||
</div>
|
||||
<div class="col s3 circle-text">
|
||||
Build the authorization URL and redirect the user to the authorization server
|
||||
</div>
|
||||
<div class="col s3 circle-text">
|
||||
After the user is redirected back to the client, verify the state
|
||||
</div>
|
||||
<div class="col s3 circle-text">
|
||||
Exchange the authorization code and code verifier for an access token
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="col s12 m7">
|
||||
<div class="card horizontal">
|
||||
<div class="card-stacked">
|
||||
<div class="card-content">
|
||||
<h6>4. Exchange the code and code verifier for token</h6>
|
||||
<p>Now that we have the authorization code, we can exchange it for an access token. This is done by sending a <b>POST</b> request to the token endpoint.</p>
|
||||
<pre class="code-block"><code id="requestUriExample"></code></pre>
|
||||
<p>With body data:</p>
|
||||
<pre class="code-block"><code id="requestBodyExample"></code></pre>
|
||||
<p6>Let's break it down...</p>
|
||||
<ul class="collection">
|
||||
<li class="collection-item">
|
||||
<p><b><span id="tokenUrl"></span></b></p>
|
||||
<p>The token endpoint URL</p>
|
||||
</li>
|
||||
<li class="collection-item">
|
||||
<p><b><span class="emphasis">grant_type</span>=<span id="grantType"></span></b></p>
|
||||
<p>The grant type, in this case <b>authorization_code</b></p>
|
||||
</li>
|
||||
<li class="collection-item">
|
||||
<p><b><span class="emphasis">client_id</span>=<span id="clientId"></span></b></p>
|
||||
<p>Client ID of the application. This is a public identifier for the client, and it is
|
||||
used by the authorization server to identify the application
|
||||
when redirecting the user back to the client.</p>
|
||||
</li>
|
||||
<li class="collection-item">
|
||||
<p><b><span class="emphasis">redirect_uri</span>=<span id="redirectUri"></span></b></p>
|
||||
<p>The redirect URI</p>
|
||||
</li>
|
||||
<li class="collection-item">
|
||||
<p><b><span class="emphasis">code</span>=<span id="code"></span></b></p>
|
||||
<p>This is the authorization code we got in the previous step and is used to obtain the
|
||||
access token.
|
||||
</p>
|
||||
</li>
|
||||
<li class="collection-item">
|
||||
<p><b><span class="emphasis">code_verifier</span>=<span id="codeVerifier"></span></b></p>
|
||||
<p>This is the code verifier we generated in the first step. It is used to verify the
|
||||
identity of the client.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="row flow-submit-container">
|
||||
<a id="get-token-btn" class="waves-effect waves-light btn full-width"
|
||||
onClick="getToken()">Get access token</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="token-result" style="display: none;" class="section">
|
||||
<div class="col s12 m7">
|
||||
<div class="card horizontal">
|
||||
<div class="card-stacked">
|
||||
<div class="card-content">
|
||||
<h6>Congratulations, you have just obtained an <b>access token</b></h6>
|
||||
<pre class="code-block"><code id="token"></code></pre>
|
||||
<h6>Let's break down what we have received...</h6>
|
||||
<ul class="collection">
|
||||
<li class="collection-item">
|
||||
<p><b>access_token</b></p>
|
||||
<p>This is the actual access token, which allows the client application to access the user's protected resources on the
|
||||
resource server (e.g., user profile, photos, etc.).
|
||||
It's encoded as a JSON Web Token (JWT), which can be decoded to reveal a header, payload, and signature.
|
||||
The payload of the JWT contains claims about the user and the authentication event. For example, the iss claim indicates
|
||||
the issuer of the token, the aud claim specifies the intended audience for the token, and the exp claim specifies the
|
||||
expiration time of the token. <a href="https://jwt.io/" target="_blank">Want to see what's inside?</a></p>
|
||||
</li>
|
||||
<li class="collection-item">
|
||||
<p><b>expires_in</b></p>
|
||||
<p>Indicates the number of seconds for which the <b>access_token</b> is valid. After this time, the access_token will expire and a
|
||||
new one must be obtained.</p>
|
||||
</li>
|
||||
<li class="collection-item">
|
||||
<p><b>refresh_expires_in</b></p>
|
||||
<p>Indicates the number of seconds for which the refresh_token is valid. Once the refresh token is expired, the client will
|
||||
need to re-authenticate the user to get a new set of tokens.</p>
|
||||
</li>
|
||||
<li class="collection-item">
|
||||
<p><b>refresh_token</b></p>
|
||||
<p>Used to obtain a new <b>access_token</b> when the current one expires. This allows the client to get a new <b>access_token</b> without
|
||||
requiring the user to log in again. Like the <b>access_token</b>, it's encoded as a JWT.</p>
|
||||
</li>
|
||||
<li class="collection-item">
|
||||
<p><b>token_type</b></p>
|
||||
<p>Indicates the type of token issued. In OAuth 2.0, the common type is "Bearer", which means that whoever bears the token
|
||||
can access the resources.</p>
|
||||
</li>
|
||||
<li class="collection-item">
|
||||
<p><b>not-before-policy</b></p>
|
||||
<p>Specifies a time before which the token should not be accepted. A value of 0 implies the token can be immediately used.</p>
|
||||
</li>
|
||||
<li class="collection-item">
|
||||
<p><b>session_state</b></p>
|
||||
<p>Represents the state of the user's session at the Authorization Server. This is the same value that could was
|
||||
returned during the Authorization Code Flow in the <b>session_state</b> parameter.
|
||||
It can be used in scenarios like OpenID Connect session management to monitor the user's session.</p>
|
||||
</li>
|
||||
<li class="collection-item">
|
||||
<p><b>scope</b></p>
|
||||
<p>Specifies the scopes granted by the user to the client application. Scopes determine the permissions associated with the
|
||||
<b>access_token</b>. Here, the granted scopes are email, offline_access, and profile. This means that with the provided access_token, the
|
||||
client application can access the user's email and profile information and is also granted offline access (typically
|
||||
used in conjunction with refresh tokens).</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>And this concludes the Authorization Code Flow with PKCE. Client application would now be able to request resources on users behalf without having to transfer his credentials with each request.</p>
|
||||
<div class="row flow-submit-container">
|
||||
<a class="waves-effect waves-light btn full-width" href="/">Try different flow</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="page-footer"></footer>
|
||||
<script src="../js/load-layout.js"></script>
|
||||
<script src="../js/cookies.js"></script>
|
||||
<script>
|
||||
const tokenEndpoint = 'https://sso.rumbuddy.cz/realms/OAuthPlayground/protocol/openid-connect/token';
|
||||
const clientID = 'oauth-playground';
|
||||
const code = new URLSearchParams(window.location.search).get('code');
|
||||
const codeVerifier = getCookie("code_verifier");
|
||||
|
||||
function fillRequestExample() {
|
||||
const requestExample =
|
||||
"grant_type=authorization_code" + "\n"
|
||||
+ "&client_id=" + clientID + "\n"
|
||||
+ "&redirect_uri=" + getRedirectUri() + "\n"
|
||||
+ "&code=" + code + "\n"
|
||||
+ "&code_verifier=" + codeVerifier;
|
||||
|
||||
$("#requestUriExample").text(tokenEndpoint);
|
||||
$("#requestBodyExample").text(requestExample);
|
||||
|
||||
$("#tokenUrl").text(tokenEndpoint);
|
||||
$("#grantType").text("authorization_code");
|
||||
$("#clientId").text(clientID);
|
||||
$("#redirectUri").text(getRedirectUri());
|
||||
$("#code").text(code);
|
||||
$("#codeVerifier").text(codeVerifier);
|
||||
}
|
||||
|
||||
function getRedirectUri() {
|
||||
return window.location.protocol + "//" + window.location.host + "/flow/pkce-3";
|
||||
}
|
||||
|
||||
function getToken() {
|
||||
const redirectURI = getRedirectUri();
|
||||
|
||||
const bodyData = new URLSearchParams();
|
||||
bodyData.append('grant_type', 'authorization_code');
|
||||
bodyData.append('client_id', clientID);
|
||||
bodyData.append('redirect_uri', getRedirectUri());
|
||||
bodyData.append('code', code);
|
||||
bodyData.append('code_verifier', codeVerifier);
|
||||
|
||||
fetch(tokenEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
|
||||
},
|
||||
body: bodyData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
$("#token-result").show();
|
||||
$("#token").text(JSON.stringify(data, null, 2));
|
||||
$("#get-token-btn").hide();
|
||||
$([document.documentElement, document.body]).animate({
|
||||
scrollTop: $("#token-result").offset().top
|
||||
}, 1000);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching the token:', error);
|
||||
});
|
||||
}
|
||||
|
||||
fillRequestExample();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -73,8 +73,8 @@
|
||||
access token, the client provides the original verifier. The server validates it against the stored challenge, ensuring
|
||||
added security against malicious interceptions.</p>
|
||||
</div>
|
||||
<div class="card-action underConstruction">
|
||||
<i class="tiny material-icons">build</i> Under construction
|
||||
<div class="card-action">
|
||||
<a href="/flow/pkce-1">Try it</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
12
src/js/cookies.js
Normal file
12
src/js/cookies.js
Normal file
@@ -0,0 +1,12 @@
|
||||
function setCookie(name, value, minutes) {
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + (minutes * 60 * 1000));
|
||||
const expires = "; expires=" + date.toUTCString();
|
||||
document.cookie = name + "=" + value + expires + "; path=/";
|
||||
}
|
||||
|
||||
function getCookie(name) {
|
||||
var value = "; " + document.cookie;
|
||||
var parts = value.split("; " + name + "=");
|
||||
if (parts.length == 2) return parts.pop().split(";").shift();
|
||||
}
|
||||
Reference in New Issue
Block a user