App authenticator docs and cli client (#66)

* move app authenticator docs from readme to separate file
* deleted openapi.yaml
* updated docs
  * replaced current authentication mechanism with JWT signature tokens
  * renamed `device_id` to `authenticator_id`
* add authenticator cli
This commit is contained in:
Niko Heller 2024-01-09 18:07:41 +01:00 committed by Giuliano Mele
parent b08d42f384
commit e2ce063d22
Signed by: MelGi
GPG key ID: E790C1211F6DEE5E
7 changed files with 1542 additions and 318 deletions

110
README.md
View file

@ -44,113 +44,3 @@ This is a fork of a great demo implementation by [@dasniko](https://github.com/d
1. Make sure that you name it "sms-2fa". This is currently a hack that will hopefully be fixed. Additional executions with other names can be added. But this first execution will be used for the confirmation SMS when setting up a new phone number.
1. Go into the config of the execution and configure the plugin so that it works with the API of your SMS proivder.
# App Authenticator
Mobile App Authenticator
## API implementation details
The API is based on Keycloaks Action Token Handler to "implement any functionality that initiates or modifies authentication session using action token handler SPI" (Ref. https://www.keycloak.org/docs/latest/server_development/index.html#_action_token_handler_spi)
OpenAPI spec (just for reference, API is managed by keycloak itself): [openapi.yaml](./openapi.yaml)
## Setup App Auth Endpoint:
`/realms/realm-id/login-actions/action-token?key=jwt&client_id=account-console&tab_id=someTabId`
### Setup Steps:
1. The URL is transmitted to the device by QR-Code and contains a one-time JWT (in case the device is not authenticated).
2. The mobile device is **supposed to use this URL** to transmit its device data.
### Required additional query parameters:
```
device_id: device ID
device_os: OS
public_key: base64 encoded public key (encoded according to the X.509)
device_push_id: device push ID (optional)
key_algorithm: public key algorithm e.g. "RSA" (case sensitive)
signature_algorithm: e.g. "SHA512withRSA"
```
Public Key is assumed to be encoded according to the X.509 standard: https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/security/spec/X509EncodedKeySpec.html
Valid Key Algorithms: https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#keyfactory-algorithms
Valid Signature Algorithms: https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#signature-algorithms
During signature validation the app authenticator instantiates a KeyFactory object with the provided key_algorithm.
The KeyFactory object will then use the public key specification (public_key) to generate a public key object.
Finally, a signature object is instantiated by signature_algorithm and initialized with the public key object to verify the message signature.
Refs:
- https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/security/KeyFactory.html#getInstance(java.lang.String)
- https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/security/KeyFactory.html#generatePublic(java.security.spec.KeySpec)
- https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/security/Signature.html#getInstance(java.lang.String)
### Response
- 201: created app authenticator
- 400: missing query params, invalid/expired JWT
## Authentication Endpoint (basically the same endpoint):
`/realms/realm-id/login-actions/action-token?key=jwt&client_id=account-console&tab_id=someTabId`
### Auth Steps:
1. The URL + JWT is transmitted to the device by firebase. (In the backend both endpoint only differ in the supplied JWT and their token handler. The setup token and auth token both grant access to a single unique server side action)
2. The mobile device is supposed to use this URL to solve the supplied challenge (see below).
### Required additional query parameters:
```
granted: true|false
```
### Signature Header
The proposed Signature header format is based on this RFC draft: https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12
String of comma separated key-values:
```
Signature: keyId:deviceId,created:unixTimestampInMilliseconds,secret:sendByFirebaseOrByChallengeEndPoint,granted:true,signature:base64encodedSignature
```
To create the signature value inside the "Signature" header, a comma seperated key-value string must be signed and base64 encoded:
```
"created:1681897832436,secret:sendByFirebaseOrByChallengeEndPoint,granted:true"
```
### Response
- 204: successfully granted/rejected
- 400: missing or invalid queryParams/signatureHeader, invalid/expired JWT
- 403: signature invalid
The firebase message or the challenge endpoint response contains a secret, which the app is supposed to send its signature back to keycloak for verification.
## How to test with openssl
1. Generate private key `openssl genpkey -algorithm ed25519 -out private.pem`
2. Generate public key `openssl pkey -in private.pem -pubout -out public.pem`
3. Use this script for signing:
```bash
#!/bin/bash
if [ -z "$1" ]; then
echo "No message supplied to be signed"
exit 1
fi
echo -n "$1" > ./.message.bin
openssl pkeyutl -sign -inkey private.pem -out .signature.bin -rawin -in .message.bin
echo ""
echo "keyId:device_id,${1},signature:$(base64 .signature.bin | tr -d '\n')"
```
The script must be called with a string parameter containing comma seperated key-values
and will return the signature header as described [here](#signature-header)
Example usage:
```wrap
./sign.sh created:1696594860241,\
secret:LG7mVUUtsPmonuCIDEe59BAAVpU9SQgoBzjtteKs31ltdGdKg2h0ywT8mBorxhYG97afSZugF0654y3kMTTWh2exC5JzekVSbJ32jcoUGveMTUFGtOl1yALxDOM2pvOvgzL0WnKBsiQbQS2u6wzL8ShCO8vbmWVxTjuD9ARaiLyP438vTVhqwmgXjd2l8Ungs78n8El2CFABahfGlKfzbfVOPk5kKgtu8iUDxhhiEawGZCBg1PmlQmaa5Lu7ecn1ZKbr5YXfBZQUcM7aSFjx8TyZeIw5yury3NiTJLl3Tr1wmb9ZwSwtusIeFB5TEx86PCw6CAZZm7wqKawW7E8sEPZUtZJxZ1CkA6M87RkedutylxjAOKvpkHfO9KdizN8OvX2G21nngFwITpnvfh3PMmZRZKvO8TD7Pvt1moXuS975ooLC51uslxvVm64YMLqWspfYTpwqEUZSVekctUWSa0DJC1859H47VKYDPS9JFOeXjd1GPGdWP,\
granted:true
created:1696594860241,\
secret:LG7mVUUtsPmonuCIDEe59BAAVpU9SQgoBzjtteKs31ltdGdKg2h0ywT8mBorxhYG97afSZugF0654y3kMTTWh2exC5JzekVSbJ32jcoUGveMTUFGtOl1yALxDOM2pvOvgzL0WnKBsiQbQS2u6wzL8ShCO8vbmWVxTjuD9ARaiLyP438vTVhqwmgXjd2l8Ungs78n8El2CFABahfGlKfzbfVOPk5kKgtu8iUDxhhiEawGZCBg1PmlQmaa5Lu7ecn1ZKbr5YXfBZQUcM7aSFjx8TyZeIw5yury3NiTJLl3Tr1wmb9ZwSwtusIeFB5TEx86PCw6CAZZm7wqKawW7E8sEPZUtZJxZ1CkA6M87RkedutylxjAOKvpkHfO9KdizN8OvX2G21nngFwITpnvfh3PMmZRZKvO8TD7Pvt1moXuS975ooLC51uslxvVm64YMLqWspfYTpwqEUZSVekctUWSa0DJC1859H47VKYDPS9JFOeXjd1GPGdWP,\
granted:true,\
signature:hgMHPxnpj9aQCD6p9KjeEr1wzqXR7eFEfRQRa0BrMzD9vFv5/+jFbLsYilQvisOajZORk9ygl32ZmvYfZ8OzBA==
```

2
app-authenticator-cli/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
node_modules
data.json

View file

@ -0,0 +1,337 @@
import { Command } from "commander";
import inquirer from "inquirer";
import axios from "axios";
import * as jose from "jose";
import { v4 as uuidv4 } from "uuid";
import fs from "fs/promises";
import { resolve, dirname } from "path";
import { fileURLToPath } from "url";
import * as AxiosLogger from "axios-logger";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const DATA_FILE = resolve(__dirname, "./data.json");
async function main() {
const program = new Command();
program
.name("authenticator-cli")
.description("CLI to test Keycloak authenticator API");
program
.command("setup")
.description(
"Generate an asymmetric keypair and complete authenticator setup"
)
.action(commandSetup);
program
.command("auth")
.description(
"poll the keycloak api for a login challenge and send reply"
)
.action(commandAuth);
try {
await program.parseAsync();
} catch (err) {
console.error("Commmand failed");
if (!axios.isAxiosError(err)) {
console.error(err);
}
}
}
// commands
async function commandSetup(_arg, _options) {
const { activationTokenUrl } = await promptActivationTokenUrl();
const url = new URL(activationTokenUrl);
// get keycloak base url
const [_matched, basePath, _realm] = url.pathname.match(
/(.*\/realms\/([^\/]+))\//
);
const baseURL = `${url.origin}${basePath}`;
// query parameters
const clientId = url.searchParams.get("client_id");
const tabId = url.searchParams.get("tab_id");
const key = url.searchParams.get("key");
// get user id from keycloak action token
const { sub: userId } = await jose.decodeJwt(key);
// prompt which signature algorithm to use and
// generate new key pair
const { alg } = await promptAlgorithm();
const { publicKey, privateKey } = await jose.generateKeyPair(alg);
const spkiPem = await jose.exportSPKI(publicKey);
const spki = spkiPem.replace(
/(?:-----(?:BEGIN|END) PUBLIC KEY-----|\s)/g,
""
);
const authenticatorId = uuidv4();
const client = new KeycloakClient({
baseURL,
privateKey,
alg,
authenticatorId,
userId,
});
await client.setup({
clientId,
tabId,
key,
publicKey: spki,
keyAlgorithm: mapKeyAlgorithm(alg),
deviceOs: "unknown",
});
const data = {
baseURL,
authenticatorId,
userId,
alg,
privateKey: await jose.exportJWK(privateKey),
publicKey: await jose.exportJWK(publicKey),
};
await saveData(data);
}
async function commandAuth(_arg, _options) {
const {
baseURL,
authenticatorId,
userId,
alg,
privateKey: privateKeyJwk,
} = await loadData();
const privateKey = await jose.importJWK(privateKeyJwk, alg);
const client = new KeycloakClient({
baseURL,
privateKey,
alg,
authenticatorId,
userId,
});
const [challenge] = await client.getChallanges();
if (!challenge) {
console.log("No current login attempts");
return;
}
console.log("Login Attempt", challenge);
const { granted } = await promptGranted();
const { codeChallenge, targetUrl } = challenge;
const url = new URL(targetUrl);
const clientId = url.searchParams.get("client_id");
const tabId = url.searchParams.get("tab_id");
const key = url.searchParams.get("key");
await client.replyChallenge({
clientId,
codeChallenge,
granted: granted.toLowerCase() === "accept",
key,
tabId,
});
}
// prompts
function promptActivationTokenUrl() {
return inquirer.prompt([
{
type: "string",
name: "activationTokenUrl",
message:
"Enter the Activation Token Url from the Keycloak Account Console:",
},
]);
}
function promptAlgorithm() {
return inquirer.prompt([
{
type: "list",
name: "alg",
message: "Select a JWK algorithm:",
choices: ["PS256", "PS512", "ES256", "ES512"],
},
]);
}
function promptGranted() {
return inquirer.prompt([
{
type: "list",
name: "granted",
message: "Accept or reject the login:",
choices: ["Accept", "Reject"],
},
]);
}
// client
/**
* @typedef {{
* userName: string
* userFirstName: string
* userLastName: string
* targetUrl: string
* secret: string
* updatedTimestamp: string
* ipAddress: string
* device: string
* browser: string
* os: string
* osVersion: string
* }} ChallengeDto
*/
class KeycloakClient {
constructor({ baseURL, privateKey, alg, authenticatorId, userId }) {
this._authenticatorId = authenticatorId;
this._userId = userId;
this._privateKey = privateKey;
this._alg = alg;
this._client = axios.create({
baseURL,
});
this._client.interceptors.request.use(AxiosLogger.requestLogger);
this._client.interceptors.response.use(
AxiosLogger.responseLogger,
(err) =>
AxiosLogger.errorLogger(err, {
params: true,
headers: true,
data: true,
status: true,
statusText: true,
})
);
}
_createSignatureToken(payload) {
return new jose.SignJWT(payload)
.setProtectedHeader({
alg: this._alg,
kid: this._authenticatorId,
typ: "JWT",
})
.setIssuedAt()
.setExpirationTime("30s")
.setSubject(this._userId)
.setJti(uuidv4())
.sign(this._privateKey);
}
async setup({
clientId,
tabId,
key,
deviceOs,
devicePushId,
publicKey,
keyAlgorithm,
}) {
const jwt = await this._createSignatureToken({ typ: "app-setup-signature-token" });
await this._client.get("/login-actions/action-token", {
headers: {
"x-signature": jwt,
},
params: {
client_id: clientId,
tab_id: tabId,
key,
authenticator_id: this._authenticatorId,
device_os: deviceOs,
device_push_id: devicePushId,
public_key: publicKey,
key_algorithm: keyAlgorithm,
},
});
}
/**
*
* @returns {Promise<ChallengeDto[]>}
*/
async getChallanges() {
const jwt = await this._createSignatureToken({
typ: "app-challenges-signature-token",
});
const { data: challenges } = await this._client.get("/challenges", {
headers: {
"x-signature": jwt,
},
});
return challenges;
}
async replyChallenge({ clientId, tabId, key, codeChallenge, granted }) {
const jwt = await this._createSignatureToken({
typ: "app-auth-signature-token",
codeChallenge,
});
await this._client.get("/login-actions/action-token", {
headers: {
"x-signature": jwt,
},
params: {
client_id: clientId,
tab_id: tabId,
key: key,
granted: granted,
},
});
}
}
// utils
async function saveData(data) {
await fs.writeFile(DATA_FILE, JSON.stringify(data, null, 4));
}
async function loadData() {
return JSON.parse(await fs.readFile(DATA_FILE));
}
/**
* map JWT signature algorithm to keycloak key algorithm
*/
function mapKeyAlgorithm(alg) {
const valueMap = {
PS256: "RSASSA-PSS",
PS512: "RSASSA-PSS",
ES256: "EC",
ES512: "EC",
};
if (valueMap[alg] === undefined) {
throw new Error(`unknown alg ${alg}`);
}
return valueMap[alg];
}
main();

875
app-authenticator-cli/package-lock.json generated Normal file
View file

@ -0,0 +1,875 @@
{
"name": "authenticator",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "authenticator",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"axios": "^1.6.2",
"axios-logger": "^2.7.0",
"commander": "^11.1.0",
"inquirer": "^9.2.12",
"jose": "^5.1.1",
"uuid": "^9.0.1"
}
},
"node_modules/@ljharb/through": {
"version": "2.3.11",
"resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.11.tgz",
"integrity": "sha512-ccfcIDlogiXNq5KcbAwbaO7lMh3Tm1i3khMPYpxlK8hH/W53zN81KM9coerRLOnTGu3nfXIniAmQbRI9OxbC0w==",
"dependencies": {
"call-bind": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ansi-escapes": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
"integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
"dependencies": {
"type-fest": "^0.21.3"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
"integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axios-logger": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/axios-logger/-/axios-logger-2.7.1.tgz",
"integrity": "sha512-slzf27jZuGwOoOeh3R7iV8PVMFsqkvYR39ohm5W3qg7+IM8MsMP4XV2V0Tl4PWKjPNB5UGZlzNZAeJoCn5GLDA==",
"dependencies": {
"chalk": "^4.1.0",
"dateformat": "^3.0.3"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/call-bind": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
"integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
"dependencies": {
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.1",
"set-function-length": "^1.1.1"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chardet": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
},
"node_modules/cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
"integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
"dependencies": {
"restore-cursor": "^3.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cli-spinners": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
"integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-width": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
"integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
"engines": {
"node": ">= 12"
}
},
"node_modules/clone": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
"integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
"engines": {
"node": ">=0.8"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
"integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
"engines": {
"node": ">=16"
}
},
"node_modules/dateformat": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz",
"integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==",
"engines": {
"node": "*"
}
},
"node_modules/defaults": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
"integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
"dependencies": {
"clone": "^1.0.2"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/define-data-property": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
"integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
"dependencies": {
"get-intrinsic": "^1.2.1",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/escape-string-regexp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/external-editor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
"integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
"dependencies": {
"chardet": "^0.7.0",
"iconv-lite": "^0.4.24",
"tmp": "^0.0.33"
},
"engines": {
"node": ">=4"
}
},
"node_modules/figures": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-5.0.0.tgz",
"integrity": "sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==",
"dependencies": {
"escape-string-regexp": "^5.0.0",
"is-unicode-supported": "^1.2.0"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/follow-redirects": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
"integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
"integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
"dependencies": {
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"dependencies": {
"get-intrinsic": "^1.1.3"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/has-property-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
"integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
"dependencies": {
"get-intrinsic": "^1.2.2"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
"integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/inquirer": {
"version": "9.2.12",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.12.tgz",
"integrity": "sha512-mg3Fh9g2zfuVWJn6lhST0O7x4n03k7G8Tx5nvikJkbq8/CK47WDVm+UznF0G6s5Zi0KcyUisr6DU8T67N5U+1Q==",
"dependencies": {
"@ljharb/through": "^2.3.11",
"ansi-escapes": "^4.3.2",
"chalk": "^5.3.0",
"cli-cursor": "^3.1.0",
"cli-width": "^4.1.0",
"external-editor": "^3.1.0",
"figures": "^5.0.0",
"lodash": "^4.17.21",
"mute-stream": "1.0.0",
"ora": "^5.4.1",
"run-async": "^3.0.0",
"rxjs": "^7.8.1",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^6.2.0"
},
"engines": {
"node": ">=14.18.0"
}
},
"node_modules/inquirer/node_modules/chalk": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"engines": {
"node": ">=8"
}
},
"node_modules/is-interactive": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
"integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==",
"engines": {
"node": ">=8"
}
},
"node_modules/is-unicode-supported": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
"integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/jose": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/jose/-/jose-5.1.3.tgz",
"integrity": "sha512-GPExOkcMsCLBTi1YetY2LmkoY559fss0+0KVa6kOfb2YFe84nAM7Nm/XzuZozah4iHgmBGrCOHL5/cy670SBRw==",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/log-symbols": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
"integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
"dependencies": {
"chalk": "^4.1.0",
"is-unicode-supported": "^0.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-symbols/node_modules/is-unicode-supported": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
"integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"engines": {
"node": ">=6"
}
},
"node_modules/mute-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz",
"integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==",
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/onetime": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
"dependencies": {
"mimic-fn": "^2.1.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ora": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
"integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==",
"dependencies": {
"bl": "^4.1.0",
"chalk": "^4.1.0",
"cli-cursor": "^3.1.0",
"cli-spinners": "^2.5.0",
"is-interactive": "^1.0.0",
"is-unicode-supported": "^0.1.0",
"log-symbols": "^4.1.0",
"strip-ansi": "^6.0.0",
"wcwidth": "^1.0.1"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ora/node_modules/is-unicode-supported": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
"integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/restore-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
"integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
"dependencies": {
"onetime": "^5.1.0",
"signal-exit": "^3.0.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/run-async": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz",
"integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/rxjs": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/set-function-length": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
"integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
"dependencies": {
"define-data-property": "^1.1.1",
"get-intrinsic": "^1.2.1",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
"integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
"dependencies": {
"os-tmpdir": "~1.0.2"
},
"engines": {
"node": ">=0.6.0"
}
},
"node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
},
"node_modules/type-fest": {
"version": "0.21.3",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/wcwidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
"integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
"dependencies": {
"defaults": "^1.0.3"
}
},
"node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
}
}
}

View file

@ -0,0 +1,21 @@
{
"name": "authenticator",
"type": "module",
"version": "1.0.0",
"description": "",
"main": "authenticator.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^1.6.2",
"axios-logger": "^2.7.0",
"commander": "^11.1.0",
"inquirer": "^9.2.12",
"jose": "^5.1.1",
"uuid": "^9.0.1"
}
}

307
docs/App Authenticator.md Normal file
View file

@ -0,0 +1,307 @@
# App Authenticator
This project is about creating an extension for Keycloak to improve two-factor authentication (2FA) by allowing users to use an authenticator app. The goal is to make the process more user-friendly. Instead of requiring users to input Time-based One-Time Password (TOTP) codes, they can simply accept or reject login attempts through the app.
During the authentication process, a cryptographic keypair generated by the client is utilized. This keypair enhances security by ensuring the authenticity of the users during the authentication process.
The extension provides the necessary API endpoints to implement such an authenticator app.
## Concept
Summary of the intended flow:
1. Activation Token Exchange: Keycloak generates an Activation Token that is passed to the client (copy paste or scanning a QR-code)
2. Key Pair Generation: The user's device generates a cryptographic key pair that is put into the device's secure storage. The public key is then registered with Keycloak, associating it with the respective user account. The client also generates and sends a unique authenticator id (uuid).
3. Challenge Transmission: When a user initiates a login attempt, Keycloak sends a challenge to the user's device. This challenge can be transmitted either through a push notification triggered by the login attempt or by polling a specific endpoint on Keycloak.
4. Challenge Signing: The user's device signs the challenge using its private key. This signed challenge is then sent back to Keycloak.
5. Verification Process: Keycloak verifies the signature with the user's public key to ascertain the authenticity of the response. Based on this verification, Keycloak can make an informed decision to either accept or reject the login attempt.
## API Documentation
### Activation Token URL
This is an example of the activation token URL that is displayed on the Keycloak My Account Console as QR-Code and copy paste option.
```plain
http://192.168.2.127:8080/realms/dev/login-actions/action-token?key=eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIxYWEzY2FhMS00ZmEwLTQzNTUtYWE1ZC1lZTVhNzc4OTA0NGYifQ.eyJleHAiOjE3MDE0Mjg0NjQsImlhdCI6MTcwMTQyODE2NCwianRpIjoiZTlhZWEyYzQtOWM4Ny00MjBkLTg2NjctNjg0YzA5MjM0ZTA3IiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguMi4xMjc6ODA4MC9yZWFsbXMvZGV2IiwiYXVkIjoiaHR0cDovLzE5Mi4xNjguMi4xMjc6ODA4MC9yZWFsbXMvZGV2Iiwic3ViIjoiYjViZjYzOTYtMjFjZC00NjZjLTk2MzMtMGRlM2ExNTJiYTYzIiwidHlwIjoiYXBwLXNldHVwLWFjdGlvbi10b2tlbiIsIm5vbmNlIjoiZTlhZWEyYzQtOWM4Ny00MjBkLTg2NjctNjg0YzA5MjM0ZTA3Iiwib2FzaWQiOiIwYTE4YTM0MS01ZWE3LTRlZjgtYmRjNS1kZTdmNDY5MjcyYjEuUldfTERIazV6QjguMjk1OTViZmEtNzU4ZS00MjFiLWE4ZDMtMGFmYjQ4ZDE3MjhkIn0.XLh09uLq9Ybx_fCIcMhWcNELy9wnPaGMZ8pRusJ_b_g&client_id=account-console&tab_id=RW_LDHk5zB8
```
**URL Parameters**
| Key | Example Value | Description |
| --------- | --------------- | ------------------------------------ |
| tab_id | RW_LDHk5zB8 | Tab Id of the User's Browser Session |
| client_id | account-console | Client ID the User is logging in |
| key | _see below_ | Keycloak Action Token as JWT |
**Decoded Keycloak Action Token**
**Header**
```json
{
"alg": "HS256",
"typ": "JWT",
"kid": "1aa3caa1-4fa0-4355-aa5d-ee5a7789044f"
}
```
**Payload**
```json
{
"exp": 1701428464,
"iat": 1701428164,
"jti": "e9aea2c4-9c87-420d-8667-684c09234e07",
"iss": "http://192.168.2.127:8080/realms/dev",
"aud": "http://192.168.2.127:8080/realms/dev",
"sub": "b5bf6396-21cd-466c-9633-0de3a152ba63",
"typ": "app-setup-action-token",
"nonce": "e9aea2c4-9c87-420d-8667-684c09234e07",
"oasid": "0a18a341-5ea7-4ef8-bdc5-de7f469272b1.RW_LDHk5zB8.29595bfa-758e-421b-a8d3-0afb48d1728d"
}
```
**Signature**
```plain
XLh09uLq9Ybx_fCIcMhWcNELy9wnPaGMZ8pRusJ_b_g
```
### Error Responses
**About HTTP Status Codes**
- `4xx` indicate a problem caused by the client.
- `401 Unauthorized` Is returned when the client failed to prove it's identity. E.g. missing authentication information, invalid credentials or a presented JWT that failed verification.
- `403 Forbidden` is returned when the client proved it's identity but is not allowed to access the resource because of missing privileges.
- `5xx`indicate a server sided problem. Error codes from this range **MAY NOT** be returned if the problem was caused by the client. Any endpoint can return with this status code.
**Error Object**
Any error response can optionally return an error object as JSON in the body with the following shape. Since it is not possible to return a customized error on the action token endpoint, only the HTTP status code is returned without a body.
```json
{
"error": "some_error_type",
"message": "details about the occurred error"
}
```
### Authentication
**Signature Tokens**
The API endpoints require an authentication mechanism that leverages client-side generated keypairs. While drafts for HTTP Message Signatures exist, they are no well-established standards.
[draft-ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures).
Instead of implementing concepts from this unfinished draft, the API uses client-side generated JSON Web Tokens (JWTs) as a form of request signature. The JWTs are signed using a private key stored on the client and transmitted in the `x-signature` header.
The client should use the authenticator id as the `kid` claim and use an asymmetric signature algorithm. The acceptable algorithms are:
- `PS512` with an `RSASSA-PSS` asymmetric key
- `ES512` with an `EC` asymmetric key
The JWT payload should contain:
- The user id as `sub` claim, which the client can extract from the `sub` claim of the action token issued by Keycloak via the activation token URL.
- An expiration time of approximately 30 seconds to mitigate replay attacks.
- A UUID for the JWT in the `jti` claim, which can be used to implement one-time tokens. The token id should be stored on the Keycloak side at least until the token expires.
- A `typ` claim similar to the Keycloak action tokens, containing a value for the corresponding endpoint or action, e.g., `get-challenges`.
- Any additional request parameters that need to be signed.
### Endpoints
#### Authenticator Setup
```
GET /realms/{realmId}/login-actions/action-token
```
##### Authentication
- The Keycloak action token (JWT) that was exchanged beforehand via the activation token URL needs to be passed in the `key` query parameter.
- The signature token (JWT) generated by the client in the `x-signature` header with claim `typ` = `app-setup-signature-token`.
**Consistency Check**
The signature token is not used for authentication here but rather for a **consistency check** to confirm that this token can be verified with the public key sent in this request
##### Parameters
| Name | In | Description |
| --------------------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `realmId` | path | The Keycloak realm ID |
| `client_id` | query | The Keycloak client id. This should always be `account-console` for the setup step. Client receives this value from the activation token URL. |
| `tab_id` | query | The Keycloak tab ID in the browser session where the user is setting up the authenticator. Client receives this value from the activation token URL. |
| `key` | query | The from Keycloak generated action token in form of a JWT. Client receives this value from the activation token URL. |
| `authenticator_id` | query | A unique ID to identify the authenticator. |
| `device_os` | query | The platform on which the authenticator app is running on. Supported values are: `android` and `ios` |
| `public_key` | query | The X.509 public key (e.g. as PKCS#8 base64 encoded) used to verify signatures |
| `key_algorithm` | query | Key algorithm of the public key |
| `device_push_id (optional)` | query | The platform specific ID to receive push notifications. For android this is the Firebase ID |
##### Responses
- `2xx` Authenticator was successfully registered
- `400 Bad Request` Missing or invalid request parameters. This includes:
- parsing of the JWT failed (invalid format)
- request parameters are invalid (e.g. the signature or key algorithm is not supported)
- `401 Unauthorized` Verification of the JWT from the `key` query parameter failed in any form (expired, missing claims, invalid signature)
- `409 Conflict` The authenticator ID is already registered
- `422 Unprocessable Entity`
Verification of the signature token failed with the given `public_key` and `key_algorithm` sent by the client.
#### Get Challenges
```
/realms/{realmId}/challenges
```
##### Authentication
- The signature token (JWT) generated by the client in the `x-signature` header with claim `typ` = `app-challenges-signature-token`.
##### Parameters
**Note:** The authenticator id is retrieved from the `kid` header of the JWT.
| Name | In | Description |
| --------- | ---- | --------------------- |
| `realmId` | path | The Keycloak realm ID |
##### Responses
- `2xx` Login challenge dtos as array
- `400 Bad Request` the `x-signature` header has a wrong format
- `401 Unauthorized` The `x-signature` header is missing or verification failed
- `412 Precondition Failed` The referenced `kid` (authenticator ID) in the signature token does not exist
##### ChallengeDTO
```json
{
"userName": "johndoe",
"userFirstName": "John",
"userLastName": "Doe",
"targetUrl": "http://192.168.2.127:8080/realms/dev/login-actions/action-token?key=eyJh...",
"codeChallenge": "FlJj9I4WoezeR3MN...",
"updatedTimestamp": 1701426908708,
"ipAddress": "192.168.2.127",
"device": "Other",
"browser": "Firefox/120.0",
"os": "Ubuntu",
"osVersion": "Unknown"
}
```
###### Target URL Example
Example of the value in `targetUrl`
```plain
http://192.168.2.127:8080/realms/dev/login-actions/action-token?key=eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIxYWEzY2FhMS00ZmEwLTQzNTUtYWE1ZC1lZTVhNzc4OTA0NGYifQ.eyJleHAiOjE3MDE0MjcyMDgsImlhdCI6MTcwMTQyNjkwOCwianRpIjoiNDNmOWFmMTItZTdjMS00NWJlLTliMDUtYTc3M2ZlNDBiNjk4IiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguMi4xMjc6ODA4MC9yZWFsbXMvZGV2IiwiYXVkIjoiaHR0cDovLzE5Mi4xNjguMi4xMjc6ODA4MC9yZWFsbXMvZGV2Iiwic3ViIjoiYjViZjYzOTYtMjFjZC00NjZjLTk2MzMtMGRlM2ExNTJiYTYzIiwidHlwIjoiYXBwLWF1dGgtYWN0aW9uLXRva2VuIiwibm9uY2UiOiI0M2Y5YWYxMi1lN2MxLTQ1YmUtOWIwNS1hNzczZmU0MGI2OTgiLCJvYXNpZCI6IjRjZTgxYTJjLTlmYjEtNDI2Ni04NjcwLWVmZjIwNzgxNjZmMS5OSzRoRTJvLTZhMC4yOTU5NWJmYS03NThlLTQyMWItYThkMy0wYWZiNDhkMTcyOGQifQ.Nc5dQmBhkuShkLJAMgoHEDdsRWz04594AtIJgCwTICM&client_id=account-console&tab_id=NK4hE2o-6a0
```
**Parameters**
| Key | Example Value | Description |
| --------- | --------------- | ------------------------------------ |
| tab_id | NK4hE2o-6a0 | Tab Id of the User's Browser Session |
| client_id | account-console | Client ID the User is logging in |
| key | _see below_ | Keycloak Action Token as JWT |
**Decoded Keycloak Action Token**
**Header**
```json
{
"alg": "HS256",
"typ": "JWT",
"kid": "1aa3caa1-4fa0-4355-aa5d-ee5a7789044f"
}
```
**Payload**
```json
{
"exp": 1701427208,
"iat": 1701426908,
"jti": "43f9af12-e7c1-45be-9b05-a773fe40b698",
"iss": "http://192.168.2.127:8080/realms/dev",
"aud": "http://192.168.2.127:8080/realms/dev",
"sub": "b5bf6396-21cd-466c-9633-0de3a152ba63",
"typ": "app-auth-action-token",
"nonce": "43f9af12-e7c1-45be-9b05-a773fe40b698",
"oasid": "4ce81a2c-9fb1-4266-8670-eff2078166f1.NK4hE2o-6a0.29595bfa-758e-421b-a8d3-0afb48d1728d"
}
```
**Signature**
```plain
Nc5dQmBhkuShkLJAMgoHEDdsRWz04594AtIJgCwTICM
```
#### Reply Challenge Endpoint
```
GET /realms/{realmId}/login-actions/action-token
```
#### Authentication
- The JWT generated by Keycloak in the `key`query parameter. It was given to the client via the challenges endpoint or push notification.
- The signature token (JWT) generated by the client in the `x-signature` header with claims:
- `typ`: `app-auth-signature-token`
- `codeChallenge`: The challenge value to sign
#### Parameters
| Name | In | Description |
| ----------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `realmId` | path | The Keycloak realm ID |
| `client_id` | query | The Keycloak client ID. This should always be `account-console` for the setup step. Client receives this value from the `targetUrl` from the ChallengeDTO. |
| `tab_id` | query | The Keycloak tab ID in the browser session where the user is setting up the authenticator. Client receives this value from the `targetUrl` from the ChallengeDTO |
| `key` | query | The from Keycloak generated action token in form of a JWT. Client receives this value from the `targetUrl` from the ChallengeDTO. |
| `granted` | query | boolean that indicates of the login attempt was granted or not |
#### Responses
- `2xx`challenge reply was successfully processed
- `400 Bad Request` Missing or invalid request parameters. This includes:
- parsing of the JWT failed (invalid format)
- the `x-signature` header has a wrong format
- `401 Unauthorized`
- The `x-signature` header is missing or signature verification failed
- The required JWT in query parameter `key` is expired or verification of the JWT failed in any other form (missing claims, invalid signature)
- `412 Precondition Failed` The referenced key (authenticator ID) in the request signature does not exist
## Development Notes
- The API is based on Keycloaks Action Token Handler to "implement any functionality that initiates or modifies authentication session using action token handler SPI" (Ref. <https://www.keycloak.org/docs/latest/server_development/index.html#_action_token_handler_spi>)
- HTTP respones from action token endpoints cannot be modified. They always return HTML
- The status code can be modifierd but the response body will be empty
- Public Key is assumed to be encoded according to the X.509 standard: https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/security/spec/X509EncodedKeySpec.html
- Valid Key Algorithms: https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#keyfactory-algorithms
- Valid Signature Algorithms: https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#signature-algorithms
- During signature validation the app authenticator instantiates a KeyFactory object with the provided key_algorithm.
The KeyFactory object will then use the public key specification (public_key) to generate a public key object.
Finally, a signature object is instantiated by signature_algorithm and initialized with the public key object to verify the message signature.
Refs:
- https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/security/KeyFactory.html#getInstance(java.lang.String)
- https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/security/KeyFactory.html#generatePublic(java.security.spec.KeySpec)
- https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/security/Signature.html#getInstance(java.lang.String)

View file

@ -1,208 +0,0 @@
openapi: 3.0.3
info:
title: Keycloak App Authenticator
description: |-
Keycloak Authentication API
All end points with required "Signature" header expect a comma seperated key-value string.
Required keys are "signature", "keyId" (device ID) and "created" (unix time in milliseconds).
e.g. keyId:device_id,created:1680253758079,signature:base64encodedSignature
see https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12
license:
name: Apache 2.0
url: http://www.apache.org/licenses/LICENSE-2.0.html
version: 1.0.0
servers:
- url: https://saml.gruene.de
paths:
/realms/{realmId}/login-actions/action-token:
get:
summary: Setup Authenticator & Authentication
tags:
- app-authenticator
description: |-
Besides the required query params when called for authenticator setup device_id, device_os, public_key,
key_algorithm and signature_algorithm are required additionally.
If push notifications are allowed, then device_push_id is required, too.
For available "key_algorithm" see:
https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#keyfactory-algorithms
For available "signature_algorithm" see:
https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#signature-algorithms
When called to authenticate Signature header and granted query param are additionally required.
The "signature" key inside Signature header should be computed on:
created:1680253758079,secret:randomString,granted=true|false
parameters:
- in: path
name: realmId
description: The realm ID
required: true
schema:
type: string
- in: query
name: key
description: Keycloak Action token (JWT)
required: true
schema:
type: string
- in: query
name: client_id
description: The current requested Client
required: true
schema:
type: string
- in: query
name: tab_id
description: Keycloak tab ID
required: true
schema:
type: string
- in: query
name: device_id
description: Mobile device ID
required: false
schema:
type: string
- in: query
name: device_push_id
description: Mobile device device push ID
required: false
schema:
type: string
- in: query
name: device_os
description: Device OS
required: false
schema:
type: string
- in: query
name: public_key
description: Base64 encoded public key
required: false
schema:
type: string
- in: query
name: key_algorithm
description: PublicKey algorithm
required: false
schema:
type: string
- in: query
name: signature_algorithm
description: PublicKey algorithm
required: false
schema:
type: string
- in: query
name: granted
description: Was access granted?
required: false
schema:
type: boolean
- in: header
name: Signature
description: Signature of the decrypted secret, which was send by keycloak + algorithm
required: false
schema:
type: string
responses:
'201':
description: Setup Authenticator successful
'204':
description: Authentication successfully accepted/denied
'400':
description: One of the required query params/headers is missing/incomplete, the device is already registered or invalid/expired JWT
'403':
description: Signature invalid
'500':
description: Failed to read user credentials (public key), malformed signature, algorithm not available, etc.
/realms/{realmId}/challenges:
get:
summary: Get current authentication challenge for this device
tags:
- app-authenticator-challenge
description: |-
The Signature header is expected as comma seperated key-value string value.
The "signature" key inside Signature header should be computed on:
created:1680253758079
parameters:
- in: path
name: realmId
description: The realm ID
required: true
schema:
type: string
- in: query
name: device_id
description: The device ID
required: true
schema:
type: string
- in: header
name: Signature
description: Signature of timestamp
required: true
schema:
type: string
responses:
'200':
description: Setup Authenticator successful
content:
application/json:
schema:
$ref: '#/components/schemas/ChallengeList'
'400':
description: Signature header is missing or incomplete
'403':
description: Signature is invalid, challenge expired
'409':
description: Device ID is not registered as second factor
'500':
description: Failed to read user credentials (public key), malformed signature, algorithm not available, etc.
components:
schemas:
ChallengeList:
type: array
items:
$ref: '#/components/schemas/Challenge'
Challenge:
type: object
properties:
userName:
type: string
description: User who is requesting authentication
userFirstName:
type: string
description: User frist name
userLastName:
type: string
description: User last name
targetUrl:
type: string
description: URL containing JWT to send challenge to
secret:
type: string
description: random string to be signed
updatedTimestamp:
type: integer
description: Unix timestamp in milliseconds the user requested authentication (login)
ipAddress:
type: string
description: IP address of the requesting device
device:
type: string
description: The requesting device, e.g. iPhone
browser:
type: string
description: Browser of the requesting device
os:
type: string
description: OS of the requesting device
osVersion:
type: string
description: OS version of the requesting device