-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use KeyCloak for application login #4
base: main
Are you sure you want to change the base?
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
import {createRemoteJWKSet, jwtVerify} from "jose"; | ||
import request from "needle"; | ||
|
||
const {AUTH_SERVER_URL, AUTH_REALM, GATEWAY_ACCOUNT_URL, GATEWAY_KEY} = process.env; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file implements a serverless lambda which exchanges an Identity Provider (IdP) token for a Gateway token. It requires the environment variables above, as listed in the PR description.
const [type, token] = req.headers.authorization?.split(" ") ?? []; | ||
if (type !== "Bearer") | ||
return sendErr(401, 'Unauthorised'); | ||
const {payload} = await jwtVerify(token, jwks) | ||
.catch(throwErr(401, 'Unauthorised')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Identity Provider token is provided as a Bearer
token in the Authorization
HTTP header, and validated using the Identity Provider JWK keyset. Note that this is performing local JWT verification and not normally making a network call to the Identity Provider; but the jose
library will periodically update the keyset from the provider.
const pid = `${AUTH_SERVER_URL}/users/${payload.sub}`; | ||
const configRes = await request('put', new URL( | ||
`/api/v1/domain/${GATEWAY.accountName}/${subdomain}`, GATEWAY.accountUrl | ||
).toString(), { | ||
useSignatures: 'public' in key, | ||
user: {'@id': pid, key} | ||
}, { | ||
json: true, | ||
auth: 'basic', | ||
username: GATEWAY.accountName, | ||
password: GATEWAY_KEY | ||
}); | ||
if (configRes.statusCode !== 200) | ||
return sendErr(configRes.statusCode, configRes.statusMessage, configRes.body); | ||
res.status(200).json({config: configRes.body, pid}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Once the user's IdP token is validated and user access is checked, the lambda calls the Gateway to obtain the subdomain configuration, which will include a newly-minted JWT for the client. If a public key has been passed as the request body, it is forwarded to the Gateway, which will enter the public key into the subdomain for signature verifications.
async function checkAccess(user, subdomain) { | ||
// TODO: Check fine-grained access to todolists, via sharing | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this demo, we not performing any user access authorisation checks against particular subdomains, except of course that the user is registered to use this app with the IdP. Such access checks could use additional features of the IdP such as KeyCloak authorisation services.
async login() { | ||
// Login removes the document fragment, so keep it in storage | ||
// See https://www.rfc-editor.org/rfc/rfc6749#section-3.1.2 | ||
if (getAppLocation() != null) { | ||
sessionStorage.setItem("docLocation", document.location.hash); | ||
setWindowURLFragment(null); | ||
} | ||
const keycloak = new Keycloak({ | ||
url: "https://app.please-open.it", | ||
realm: "5d970145-a48d-4e9b-84ab-15de19f9d3a5", | ||
clientId: window.location.hostname | ||
}); | ||
await keycloak.init({ | ||
onLoad: "login-required", | ||
flow: "implicit", | ||
checkLoginIframe: false | ||
}); | ||
let docLocation = sessionStorage.getItem("docLocation"); | ||
if (docLocation != null) { | ||
sessionStorage.removeItem("docLocation"); | ||
setWindowURLFragment(docLocation); | ||
} | ||
return keycloak.token; | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new login method uses the KeyCloak Javascript adapter to login to the IdP. The only complication here is that this app uses the URL fragment for identification of the TodoList (its subdomain name), which clashes with the fragments used when the IdP redirects back to the application. To avoid this problem we use session storage to re-instigate the original fragment after login.
let {documentId, filter} = getAppLocation() ?? {}; | ||
const isNew = !documentId; | ||
if (isNew) { | ||
documentId = uuid(); | ||
if (!documentId) { | ||
documentId = (documentId ?? localStorage.getItem("lastTodoList")) || uuid(); | ||
setAppLocation(documentId, filter); | ||
} | ||
localStorage.setItem("lastTodoList", documentId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here we change the behaviour of the app to redirect itself back to the user's last TodoList if no URL fragment was specified. This is possible because the use of Named Subdomains in the Gateway means the TodoLists are stored there. This is a significant upgrade to the application: a user can navigate to their stored TodoLists any time on any device, without having to have them open elsewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This new Javascript file wraps up the keystore-idb library (introduced as a global, window.keystore.default
, by the script in index.html) as a Device
. In particular, a Device can present itself to m-ld as an Initial App with a transport security member for signing messages, as described in the Gateway documentation.
const device = await Device.here(); | ||
// Exchange the access token for gateway domain configuration | ||
const configRes = await fetch(`/api/config?domain=${this.id}`, { | ||
method: 'POST', | ||
headers: {'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json'}, | ||
body: JSON.stringify(device.key) | ||
}); | ||
const {config, pid} = await configRes.json(); | ||
const meld = await clone( | ||
new MemoryLevel, | ||
IoRemotes, | ||
config | ||
{'@id': uuid(), ...config}, | ||
device.asMeldApp(pid) | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here we replace the hard-wired clone configuration with the configuration received from the lambda. No special handling is needed for the Gateway JWT embedded in the configuration, we just pass it all to m-ld, remembering to add the @id
for the clone. We also provide the signatures implementation to m-ld via the "device".
💡 Note this branch & pull request are intended to be long-lived, as they show modifications to the
main
branch for a specific feature.This branch shows how to use an Identity Provider and a keystore for user login and signatures. See the PR change comments for additional information. Note that while these features are combined here, it would be straightforward to use Identity Provider login without also implementing signatures.
identity provider login
This feature is implemented using Gateway JWT authentication tokens, as described in the Gateway documentation.
The chosen Identity Provider is KeyCloak, as an exemplar of an open-source OpenID Connect service. Required configuration items for running the demo with your own KeyCloak instance are given below.
For our deployment at m-ld-todomvc-vanillajs-git-user-accounts-m-ld.vercel.app, we have created a realm with the free online KeyCloak service provided by Please Open It. If you want to try the deployed app, please email info@m-ld.org and we'll register you.
The architecture uses a serverless lambda to exchange KeyCloak tokens for Gateway tokens. This can be found in the
/api
folder, and works out of the box with Vercel serverless functions. The code can be easily ported to another serverless provider or a web framework like Express.js.The following configuration items are required for the KeyCloak integration. These can be found:
app.js
url
AUTH_SERVER_URL
https://app.please-open.it/auth
realm
AUTH_REALM
5d970145-a48d-4e9b-84ab-15de19f9d3a5
clientId
(uses page hostname by default)m-ld-todomvc-vanillajs-git-user-accounts-m-ld.vercel.app
GATEWAY_ACCOUNT_URL
https://gw.m-ld.org/m-ld-todomvc-vanillajs
GATEWAY_KEY
appid.lccfcj:...
The KeyCloak client configuration in this demo is straightforward and as recommended for use with the KeyCloak Javascript adapter. For reference, the configuration for the Client ID given above is attached: m-ld-todomvc-vanillajs-git-user-accounts-m-ld.vercel.app.json.
signatures
This feature is implemented using a client-side library to generate and store RSA public keys for the local browser ("device"), integrated with the Gateway support for digital signatures.
The library is keystore-idp, which wraps the browser's native ability to store unexportable private CryptoKey objects in IndexedDB.
See the comments in Files changed for further information.
Note: requires @m-ld/m-ld-gateway@0.1.0-edge.3