Skip to content

Commit

Permalink
chore: random failing example
Browse files Browse the repository at this point in the history
  • Loading branch information
douglasduteil committed Sep 25, 2024
1 parent 7cbb3cc commit fe39369
Show file tree
Hide file tree
Showing 16 changed files with 491 additions and 0 deletions.
21 changes: 21 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org

root = true


[*]

# Change these settings to your own preference
indent_style = space
indent_size = 2

# We recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false
61 changes: 61 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#

name: E2E

# Automatically cancel in-progress actions on the same branch
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request_target' && github.head_ref || github.ref }}
cancel-in-progress: true

on:
push:
workflow_dispatch:

jobs:
e2e:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7

- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 #v4.0.4
with:
node-version: "node"

- name: 🌭 Install bun
uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2

- run: bun install --frozen-lockfile

- name: cypress-io/github-action needs package-lock.json
run: |
touch package-lock.json
- name: Cypress run
uses: cypress-io/github-action@0da3c06ed8217b912deea9d8ee69630baed1737e # v6.7.6
with:
build: bun run build
install-command: bun install --frozen-lockfile
spec: apps/${{ matrix.e2e_test }}/features
start: bun run start

- uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
if: failure()
with:
compression-level: 9
name: cypress-${{ matrix.e2e_test }}-screenshots
path: e2e/cypress/screenshots
spec: features/${{ matrix.e2e_test }}

- uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
if: failure()
with:
compression-level: 9
name: cypress-${{ matrix.e2e_test }}-videos
path: e2e/cypress/videos

strategy:
matrix:
e2e_test:
- spa_pkce_proconnect
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#

dist
node_modules
15 changes: 15 additions & 0 deletions apps/spa_pkce_proconnect/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# spa_pkce_proconnect

To install dependencies:

```bash
bun install
```

To run:

```bash
bun run index.ts
```

This project was created using `bun init` in bun v1.1.29. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
10 changes: 10 additions & 0 deletions apps/spa_pkce_proconnect/features/main.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#language: fr
Fonctionnalité: Spa PKCE Test

Scénario: Connexion avec succès
Soit la page de démarrage
* je clique sur "SPA PKCE"
Quand je clique sur "Cliquez pour vous connecter"
* je remplis le formulaire de connexion
* je clique sur "Connexion"
Alors je vois le message "Connexion réussie"
72 changes: 72 additions & 0 deletions apps/spa_pkce_proconnect/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<html>
<title>OAuth Authorization Code + PKCE in Vanilla JS</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1, user-scalable=no"
/>

<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>

<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>
12 changes: 12 additions & 0 deletions apps/spa_pkce_proconnect/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "spa_pkce_proconnect",
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^5.4.8"
}
}
174 changes: 174 additions & 0 deletions apps/spa_pkce_proconnect/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
//////////////////////////////////////////////////////////////////////
// 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 = "";
}

// 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 = "";
}
);
}

// Clean these up since we don't need them anymore
localStorage.removeItem("pkce_state");
localStorage.removeItem("pkce_code_verifier");
}

//////////////////////////////////////////////////////////////////////
// GENERAL HELPER FUNCTIONS

// 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"
);
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 {};
}
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(
""
);
}

// 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);
}

// 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(/=+$/, "");
}

// Return the base64-urlencoded sha256 hash for the PKCE challenge
async function pkceChallengeFromVerifier(v) {
hashed = await sha256(v);
return base64urlencode(hashed);
}
4 changes: 4 additions & 0 deletions apps/spa_pkce_proconnect/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"compilerOptions": {},
"extends": "@tsconfig/bun/tsconfig.json"
}
Binary file added bun.lockb
Binary file not shown.
Loading

0 comments on commit fe39369

Please sign in to comment.