-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
15 changed files
with
950 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
vendor | ||
readme.txt | ||
release | ||
/svn/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
ISSUER_URL="https://localhost:8443" | ||
CLIENT_ID="oidc-server-plugin-tests" | ||
CLIENT_SECRET="oidc-server-plugin-tests" | ||
TLS_CA_CERT="../../matrix-oidc-playground/tls/ca/rootCA.pem" | ||
TLS_CERT="../../matrix-oidc-playground/tls/tls.pem" | ||
TLS_KEY="../../matrix-oidc-playground/tls/tls-key.pem" | ||
APP_BASE_URL="https://localhost:7443" | ||
WORDPRESS_USER="admin" | ||
WORDPRESS_PASS="password" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
/.yarn | ||
/node_modules | ||
.env.local |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
yarnPath: .yarn/releases/yarn-1.22.19.cjs |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# End-to-end tests | ||
|
||
Running theses tests requires having [matrix-oidc-playground](https://github.com/Automattic/matrix-oidc-playground/) running in the same machine as the tests. Make sure to follow the setup instructions there before running the tests. | ||
|
||
Once you have matrix-oidc-playground running, simply run: | ||
|
||
```shell | ||
composer test | ||
``` | ||
|
||
The tests pass when the output ends with something like: | ||
|
||
|
||
```shell | ||
JWT token { | ||
iss: 'https://localhost:8443/', | ||
sub: 'admin', | ||
aud: 'oidc-server-plugin-tests', | ||
iat: 1695316090, | ||
exp: 1695319690, | ||
auth_time: 1695316090, | ||
nonce: '7926217c4ad37e6db5cc8e6f78a421ed' | ||
} | ||
userinfo { | ||
scope: 'openid profile', | ||
username: 'admin', | ||
name: 'admin', | ||
nickname: 'admin', | ||
picture: 'https://secure.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=96&d=mm&r=g', | ||
sub: 'admin' | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
declare global { | ||
namespace NodeJS { | ||
interface ProcessEnv { | ||
ISSUER_URL: string, | ||
CLIENT_ID: string, | ||
CLIENT_SECRET: string, | ||
TLS_CA_CERT: string, | ||
TLS_CERT: string, | ||
TLS_KEY: string, | ||
APP_BASE_URL: string, | ||
WORDPRESS_USER: string, | ||
WORDPRESS_PASS: string, | ||
} | ||
} | ||
} | ||
|
||
export {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import fs from "node:fs"; | ||
import path from "node:path"; | ||
import dotenv from "dotenv" | ||
import {OpenIdClient} from "./src/OpenIdClient"; | ||
import {HttpsServer} from "./src/HttpsServer"; | ||
import {HttpsClient} from "./src/HttpsClient"; | ||
import crypto from "crypto"; | ||
import { parse as parseHtml } from 'node-html-parser'; | ||
import {AxiosResponse} from "axios"; | ||
|
||
dotenv.config({ path: ".env" }); | ||
if (fs.existsSync(".env.local")) { | ||
dotenv.config({ path: ".env.local", override: true }); | ||
} | ||
|
||
let httpsServer: HttpsServer; | ||
|
||
async function run() { | ||
const env = process.env; | ||
if (!env.ISSUER_URL || !env.CLIENT_ID || !env.CLIENT_SECRET || !env.TLS_CA_CERT || !env.TLS_CERT || !env.TLS_KEY || !env.APP_BASE_URL || !env.WORDPRESS_USER || !env.WORDPRESS_PASS) { | ||
console.error("Some or all required environment variables were not defined. Set them in the .env file."); | ||
process.exit(1); | ||
} | ||
|
||
const caCert = fs.readFileSync(path.resolve(env.TLS_CA_CERT)); | ||
|
||
const openIdClient = new OpenIdClient({ | ||
issuerUrl: env.ISSUER_URL, | ||
clientId: env.CLIENT_ID, | ||
clientSecret: env.CLIENT_SECRET, | ||
redirectUri: env.APP_BASE_URL, | ||
caCert, | ||
}); | ||
|
||
const httpsClient = new HttpsClient({ | ||
caCert, | ||
}) | ||
|
||
httpsServer = new HttpsServer({ | ||
baseUrl: new URL(env.APP_BASE_URL), | ||
tlsCert: fs.readFileSync(path.resolve(env.TLS_CERT)), | ||
tlsKey: fs.readFileSync(path.resolve(env.TLS_KEY)), | ||
}); | ||
httpsServer.start(); | ||
|
||
const state = crypto.randomBytes(16).toString("hex"); | ||
const nonce = crypto.randomBytes(16).toString("hex"); | ||
|
||
// Generate authorization URL. | ||
const authorizationUrl = await openIdClient.authorizationUrl(state, nonce); | ||
|
||
// Call authorization URL. | ||
let response = await httpsClient.get(authorizationUrl); | ||
let responseUrl = new URL(response.config.url ?? ""); | ||
|
||
// Get promise to next server request. | ||
let serverRequest = httpsServer.once(); | ||
|
||
// Log in. | ||
if (response.status === 200 && responseUrl.toString().includes("wp-login.php")) { | ||
response = await httpsClient.post(new URL(`${env.ISSUER_URL}/wp-login.php`), { | ||
testcookie: "1", | ||
log: env.WORDPRESS_USER, | ||
pwd: env.WORDPRESS_PASS, | ||
redirect_to: responseUrl.searchParams.get("redirect_to"), | ||
}); | ||
} | ||
|
||
// Grant authorization. | ||
await grantAuthorization(httpsClient, env.ISSUER_URL ?? "", response); | ||
|
||
// Get access token. | ||
const request = await serverRequest; | ||
const tokenSet = await openIdClient.exchangeCodeForToken(request); | ||
console.log("JWT token", parseJwt(tokenSet.id_token ?? "")); | ||
|
||
// Get userinfo. | ||
const userinfo = await openIdClient.userinfo(tokenSet.access_token ?? ""); | ||
console.debug("userinfo", userinfo); | ||
} | ||
|
||
async function grantAuthorization(httpsClient: HttpsClient, issuerUrl: string, response: AxiosResponse): Promise<AxiosResponse> { | ||
const authorizeButtonMarkup = '<input type="submit" name="authorize" class="button button-primary button-large" value="Authorize"/>'; | ||
if (response.status !== 200 || !response.data.includes(authorizeButtonMarkup)) { | ||
// Nothing to do, we were not shown the grant authorization screen. | ||
return response; | ||
} | ||
|
||
const html = parseHtml(response.data); | ||
let inputFields = html.querySelector("form")?.querySelectorAll("input"); | ||
if (!inputFields || inputFields.length === 0) { | ||
throw "Authorization form not found"; | ||
} | ||
|
||
inputFields = inputFields.filter(field => ["hidden", "submit"].includes(field.attrs.type)); | ||
const params = {}; | ||
// @ts-ignore | ||
inputFields.forEach(field => params[field.attrs.name] = field.attrs.value); | ||
|
||
return httpsClient.post(new URL(`${issuerUrl}/wp-json/openid-connect/authorize`), params); | ||
} | ||
|
||
function parseJwt(token: string) { | ||
const base64Url = token.split('.')[1]; | ||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); | ||
const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) { | ||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); | ||
}).join('')); | ||
return JSON.parse(jsonPayload); | ||
} | ||
|
||
void run().catch(error => { | ||
console.error(error); | ||
process.exit(1); | ||
}).finally(() => { | ||
if (httpsServer) { | ||
void httpsServer.stop(); | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
{ | ||
"packageManager": "[email protected]", | ||
"name": "openid-connect-server-integration-tests", | ||
"version": "1.0.0", | ||
"main": "index.ts", | ||
"license": "MIT", | ||
"type": "commonjs", | ||
"scripts": { | ||
"start": "ts-node-esm index.ts" | ||
}, | ||
"devDependencies": { | ||
"@types/node": "^20.6.2", | ||
"ts-node": "^10.9.1", | ||
"typescript": "^5.2.2" | ||
}, | ||
"dependencies": { | ||
"axios": "^1.5.0", | ||
"dotenv": "^16.3.1", | ||
"http-terminator": "^3.2.0", | ||
"node-html-parser": "^6.1.10", | ||
"openid-client": "^5.5.0", | ||
"set-cookie-parser": "^2.6.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import https from "node:https"; | ||
import axios, {AxiosInstance, AxiosResponse} from "axios"; | ||
const setCookieParser = require('set-cookie-parser'); | ||
|
||
type Options = { | ||
caCert: Buffer, | ||
} | ||
|
||
export class HttpsClient { | ||
private readonly axios: AxiosInstance; | ||
private cookies: string[] = []; | ||
|
||
constructor(private readonly options: Options) { | ||
this.axios = axios.create({ | ||
httpsAgent: new https.Agent({ ca: this.options.caCert }), | ||
maxRedirects: 0, | ||
validateStatus: function (status) { | ||
return [200, 302].includes(status); | ||
} | ||
}); | ||
|
||
this.axios.interceptors.response.use(response => { | ||
console.debug("response", response.status, response.config.url, "\n") | ||
return response; | ||
}); | ||
|
||
this.axios.interceptors.request.use(request => { | ||
console.debug("request", request.url, request.data ?? "", "\n") | ||
return request; | ||
}); | ||
} | ||
|
||
async get(url: URL): Promise<AxiosResponse> { | ||
const response = await this.axios.get(url.toString(), { | ||
headers: { | ||
Cookie: this.cookieHeader(), | ||
}, | ||
}); | ||
|
||
this.setCookies(response); | ||
|
||
if (response.status === 302) { | ||
return this.get(response.headers.location); | ||
} | ||
|
||
return response; | ||
} | ||
|
||
async post(url: URL, data: object): Promise<AxiosResponse> { | ||
const formData = new FormData(); | ||
for (const property in data) { | ||
// @ts-ignore | ||
formData.append(property, data[property]); | ||
} | ||
|
||
const response = await this.axios.post(url.toString(), formData, { | ||
headers: { | ||
Cookie: this.cookieHeader(), | ||
}, | ||
}); | ||
|
||
this.setCookies(response); | ||
|
||
if (response.status === 302) { | ||
return this.get(response.headers.location); | ||
} | ||
|
||
return response | ||
} | ||
|
||
private setCookies(response: AxiosResponse) { | ||
const cookies = setCookieParser.parse(response); | ||
for (const cookie of cookies) { | ||
this.cookies[cookie.name] = cookie.value; | ||
} | ||
} | ||
|
||
private cookieHeader(): string { | ||
let header = ""; | ||
for (const name in this.cookies) { | ||
const value = this.cookies[name]; | ||
if (value.trim() === "") { | ||
continue; | ||
} | ||
if (header !== "") { | ||
header += "; "; | ||
} | ||
header += `${encodeURIComponent(name)}=${encodeURIComponent(this.cookies[name])}`; | ||
} | ||
return header; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import http, {IncomingMessage, ServerResponse} from "node:http"; | ||
import https, {Server as BaseServer} from "node:https"; | ||
import {createHttpTerminator, HttpTerminator} from "http-terminator"; | ||
|
||
type Options = { | ||
baseUrl: URL, | ||
tlsCert: Buffer, | ||
tlsKey: Buffer, | ||
}; | ||
|
||
export class HttpsServer { | ||
private readonly server: BaseServer<typeof http.IncomingMessage, typeof http.ServerResponse>; | ||
private readonly terminator: HttpTerminator; | ||
|
||
constructor(private readonly options: Options) { | ||
this.server = https.createServer({ | ||
key: options.tlsKey, | ||
cert: options.tlsCert, | ||
}); | ||
this.terminator = createHttpTerminator({server: this.server}); | ||
} | ||
|
||
async once(): Promise<IncomingMessage> { | ||
return new Promise((resolve, reject) => { | ||
this.server.once("error", error => reject(error)); | ||
this.server.once("request", (request, response) => { | ||
response.end(); | ||
resolve(request); | ||
}); | ||
}); | ||
} | ||
|
||
start() { | ||
// @ts-ignore | ||
this.server.listen(this.options.baseUrl.port, this.options.baseUrl.hostname, () => { | ||
console.info(`Server listening at ${this.options.baseUrl.toString()}`); | ||
}); | ||
} | ||
|
||
async stop() { | ||
this.server.removeAllListeners(); | ||
void this.terminator.terminate(); | ||
} | ||
} |
Oops, something went wrong.