Skip to content

Commit

Permalink
ci(github): e2e debug
Browse files Browse the repository at this point in the history
  • Loading branch information
douglasduteil committed Sep 25, 2024
1 parent fe39369 commit 8368762
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 224 deletions.
74 changes: 11 additions & 63 deletions apps/spa_pkce_proconnect/index.html
Original file line number Diff line number Diff line change
@@ -1,72 +1,20 @@
<html>
<title>OAuth Authorization Code + PKCE in Vanilla JS</title>
<title>SPA PKCE ProConnect</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1, user-scalable=no"
/>
<link rel="stylesheet" href="https://unpkg.com/mvp.css" />

<script>
// Configure your application and authorization server details
var config = {
client_id: "",
redirect_uri: "http://localhost:8080/",
authorization_endpoint: "",
token_endpoint: "",
requested_scopes: "",
};
</script>

<div class="flex-center full-height">
<div class="content">
<a href="#" id="start">Cliquez pour vous connecter</a>
<div id="token" class="hidden">
<h2>Access Token</h2>
<div id="access_token" class="code"></div>
</div>
<div id="error" class="hidden">
<h2>Error</h2>
<div id="error_details" class="code"></div>
</div>
</div>
</div>
<main>
<header>
<h1>SPA PKCE ProConnect</h1>
<p>Cette application utilise OAuth Authorization Code + PKCE.</p>
<p>
<a href="#" id="login"><b>Cliquez pour vous connecter</b></a>
</p>
</header>
</main>

<script type="module" src="/src/main.ts"></script>
<style>
body {
padding: 0;
margin: 0;
min-height: 100vh;
font-family: arial, sans-serif;
}
@media (max-width: 400px) {
body {
padding: 10px;
}
}
.full-height {
min-height: 100vh;
}
.flex-center {
align-items: center;
display: flex;
justify-content: center;
}
.content {
max-width: 400px;
}
h2 {
text-align: center;
}
.code {
font-family: "Courier New", "Courier", monospace;
width: 100%;
padding: 4px;
border: 1px #ccc solid;
border-radius: 4px;
word-break: break-all;
}
.hidden {
display: none;
}
</style>
</html>
3 changes: 2 additions & 1 deletion apps/spa_pkce_proconnect/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"preview": "vite preview"
},
"devDependencies": {
"vite": "^5.4.8"
"oauth4webapi": "2.17.0",
"vite": "5.4.8"
}
}
270 changes: 112 additions & 158 deletions apps/spa_pkce_proconnect/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,174 +1,128 @@
//////////////////////////////////////////////////////////////////////
// OAUTH REQUEST

// Initiate the PKCE Auth Code flow when the link is clicked
document.getElementById("start")!.addEventListener("click", async function (e) {
e.preventDefault();

// Create and store a random "state" value
var state = generateRandomString();
localStorage.setItem("pkce_state", state);

// Create and store a new PKCE code_verifier (the plaintext random secret)
var code_verifier = generateRandomString();
localStorage.setItem("pkce_code_verifier", code_verifier);

// Hash and base64-urlencode the secret to use as the challenge
var code_challenge = await pkceChallengeFromVerifier(code_verifier);

// Build the authorization URL
var url =
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
window.location = url;
});

//////////////////////////////////////////////////////////////////////
// OAUTH REDIRECT HANDLING

// Handle the redirect back from the authorization server and
// get an access token from the token endpoint

var q = parseQueryString(window.location.search.substring(1));

// Check if the server returned an error string
if (q.error) {
alert("Error returned from authorization server: " + q.error);
document.getElementById("error_details").innerText =
q.error + "\n\n" + q.error_description;
document.getElementById("error").classList = "";
}
//
// Inspired by
// from https://github.com/panva/oauth4webapi/blob/v2.17.0/examples/public.ts
//

import * as oauth from "oauth4webapi";

// Prerequisites

let issuer = new URL("https://fca.integ01.dev-agentconnect.fr/api/v2"); // Authorization server's Issuer Identifier URL
let algorithm = "oidc" as const;
let client_id = "74b409f6-a364-4a9b-bd6f-42b549e99d72";

document.getElementById("login")?.addEventListener("click", login);

/**
* Value used in the authorization request as redirect_uri pre-registered at the Authorization
* Server.
*/
let redirect_uri!: string;

// End of prerequisites

const as = await oauth
.discoveryRequest(issuer, { algorithm })
.then((response) => oauth.processDiscoveryResponse(issuer, response));

const client: oauth.Client = {
client_id,
token_endpoint_auth_method: "none",
};

const code_challenge_method = "S256";
/**
* The following MUST be generated for every redirect to the authorization_endpoint. You must store
* the code_verifier and nonce in the end-user session such that it can be recovered as the user
* gets redirected from the authorization server back to your application.
*/
const code_verifier = oauth.generateRandomCodeVerifier();
const code_challenge = await oauth.calculatePKCECodeChallenge(code_verifier);
let state: string | undefined;

async function login() {
// redirect user to as.authorization_endpoint
const authorizationUrl = new URL(as.authorization_endpoint!);
authorizationUrl.searchParams.set("client_id", client.client_id);
authorizationUrl.searchParams.set("redirect_uri", redirect_uri);
authorizationUrl.searchParams.set("response_type", "code");
authorizationUrl.searchParams.set("scope", "api:read");
authorizationUrl.searchParams.set("code_challenge", code_challenge);
authorizationUrl.searchParams.set(
"code_challenge_method",
code_challenge_method,
);

// If the server returned an authorization code, attempt to exchange it for an access token
if (q.code) {
// Verify state matches what we set at the beginning
if (localStorage.getItem("pkce_state") != q.state) {
alert("Invalid state");
} else {
// Exchange the authorization code for an access token
sendPostRequest(
config.token_endpoint,
{
grant_type: "authorization_code",
code: q.code,
client_id: config.client_id,
redirect_uri: config.redirect_uri,
code_verifier: localStorage.getItem("pkce_code_verifier"),
},
function (request, body) {
// Initialize your application now that you have an access token.
// Here we just display it in the browser.
document.getElementById("access_token").innerText = body.access_token;
document.getElementById("start").classList = "hidden";
document.getElementById("token").classList = "";

// Replace the history entry to remove the auth code from the browser address bar
window.history.replaceState({}, null, "/");
},
function (request, error) {
// This could be an error response from the OAuth server, or an error because the
// request failed such as if the OAuth server doesn't allow CORS requests
document.getElementById("error_details").innerText =
error.error + "\n\n" + error.error_description;
document.getElementById("error").classList = "";
}
);
/**
* We cannot be sure the AS supports PKCE so we're going to use state too. Use of PKCE is
* backwards compatible even if the AS doesn't support it which is why we're using it regardless.
*/
if (as.code_challenge_methods_supported?.includes("S256") !== true) {
state = oauth.generateRandomState();
authorizationUrl.searchParams.set("state", state);
}

// Clean these up since we don't need them anymore
localStorage.removeItem("pkce_state");
localStorage.removeItem("pkce_code_verifier");
// now redirect the user to authorizationUrl.href
window.location.href = authorizationUrl.href;
}

//////////////////////////////////////////////////////////////////////
// GENERAL HELPER FUNCTIONS
// one eternity later, the user lands back on the redirect_uri
// Authorization Code Grant Request & Response
async function handle_authorization_code_grant_response() {
let access_token: string;
const currentUrl: URL = new URL(location.href);
const params = oauth.validateAuthResponse(as, client, currentUrl, state);
if (oauth.isOAuth2Error(params)) {
console.error("Error Response", params);
throw new Error(); // Handle OAuth 2.0 redirect error
}

// Make a POST request and parse the response as JSON
function sendPostRequest(url, params, success, error) {
var request = new XMLHttpRequest();
request.open("POST", url, true);
request.setRequestHeader(
"Content-Type",
"application/x-www-form-urlencoded; charset=UTF-8"
const response = await oauth.authorizationCodeGrantRequest(
as,
client,
params,
redirect_uri,
code_verifier,
);
request.onload = function () {
var body = {};
try {
body = JSON.parse(request.response);
} catch (e) {}

if (request.status == 200) {
success(request, body);
} else {
error(request, body);
}
};
request.onerror = function () {
error(request, {});
};
var body = Object.keys(params)
.map((key) => key + "=" + params[key])
.join("&");
request.send(body);
}

// Parse a query string into an object
function parseQueryString(string) {
if (string == "") {
return {};
let challenges: oauth.WWWAuthenticateChallenge[] | undefined;
if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) {
for (const challenge of challenges) {
console.error("WWW-Authenticate Challenge", challenge);
}
throw new Error(); // Handle WWW-Authenticate Challenges as needed
}
var segments = string.split("&").map((s) => s.split("="));
var queryString = {};
segments.forEach((s) => (queryString[s[0]] = s[1]));
return queryString;
}

//////////////////////////////////////////////////////////////////////
// PKCE HELPER FUNCTIONS

// Generate a secure random string using the browser crypto functions
function generateRandomString() {
var array = new Uint32Array(28);
window.crypto.getRandomValues(array);
return Array.from(array, (dec) => ("0" + dec.toString(16)).substr(-2)).join(
""
const result = await oauth.processAuthorizationCodeOAuth2Response(
as,
client,
response,
);
}
if (oauth.isOAuth2Error(result)) {
console.error("Error Response", result);
throw new Error(); // Handle OAuth 2.0 response body error
}

// Calculate the SHA256 hash of the input text.
// Returns a promise that resolves to an ArrayBuffer
function sha256(plain) {
const encoder = new TextEncoder();
const data = encoder.encode(plain);
return window.crypto.subtle.digest("SHA-256", data);
console.log("Access Token Response", result);
({ access_token } = result);
}

// Base64-urlencodes the input string
function base64urlencode(str) {
// Convert the ArrayBuffer to string using Uint8 array to conver to what btoa accepts.
// btoa accepts chars only within ascii 0-255 and base64 encodes them.
// Then convert the base64 encoded to base64url encoded
// (replace + with -, replace / with _, trim trailing =)
return btoa(String.fromCharCode.apply(null, new Uint8Array(str)))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
// Protected Resource Request
async function handle_protected_resource_request() {
let access_token!: string;
const response = await oauth.protectedResourceRequest(
access_token,
"GET",
new URL("https://rs.example.com/api"),
);

let challenges: oauth.WWWAuthenticateChallenge[] | undefined;
if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) {
for (const challenge of challenges) {
console.error("WWW-Authenticate Challenge", challenge);
}
throw new Error(); // Handle WWW-Authenticate Challenges as needed
}

// Return the base64-urlencoded sha256 hash for the PKCE challenge
async function pkceChallengeFromVerifier(v) {
hashed = await sha256(v);
return base64urlencode(hashed);
console.log("Protected Resource Response", await response.json());
}
Binary file modified bun.lockb
Binary file not shown.
4 changes: 2 additions & 2 deletions cypress/support/step_definitions/general.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ When("je clique sur {string}", function (text: string) {
cy.contains(text).click();
});

When("je remplis le formulaire de connexion", function (string: string) {
return "pending";
When("je remplis le formulaire de connexion", function () {
cy.get("input").click();
});

Then("je vois {string}", function (text: string) {
Expand Down

0 comments on commit 8368762

Please sign in to comment.