Use configuration url to retrieve oidc endpoints

The /.well-known/openid-configuration endpoint can be used to
retrieve the majority of configuration needed for oidc

Additionally refactor the callback url to be generated on the server
side as this is a fixed endpoint.

Add linting fixes
This commit is contained in:
Rory Powell 2021-07-05 17:16:45 +01:00
parent baab7141c0
commit 7803540399
5 changed files with 65 additions and 38 deletions

View File

@ -2,7 +2,14 @@ const passport = require("koa-passport")
const LocalStrategy = require("passport-local").Strategy const LocalStrategy = require("passport-local").Strategy
const JwtStrategy = require("passport-jwt").Strategy const JwtStrategy = require("passport-jwt").Strategy
const { StaticDatabases } = require("./db/utils") const { StaticDatabases } = require("./db/utils")
const { jwt, local, authenticated, google, oidc, auditLog } = require("./middleware") const {
jwt,
local,
authenticated,
google,
oidc,
auditLog,
} = require("./middleware")
const { setDB, getDB } = require("./db") const { setDB, getDB } = require("./db")
// Strategies // Strategies

View File

@ -1,6 +1,7 @@
const env = require("../../environment") const env = require("../../environment")
const jwt = require("jsonwebtoken") const jwt = require("jsonwebtoken")
const database = require("../../db") const database = require("../../db")
const fetch = require("node-fetch")
const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy
const { const {
StaticDatabases, StaticDatabases,
@ -9,7 +10,17 @@ const {
} = require("../../db/utils") } = require("../../db/utils")
// async function authenticate(token, tokenSecret, profile, done) { // async function authenticate(token, tokenSecret, profile, done) {
async function authenticate(issuer, sub, profile, jwtClaims, accessToken, refreshToken, idToken, params, done) { async function authenticate(
issuer,
sub,
profile,
jwtClaims,
accessToken,
refreshToken,
idToken,
params,
done
) {
// Check the user exists in the instance DB by email // Check the user exists in the instance DB by email
const db = database.getDB(StaticDatabases.GLOBAL.name) const db = database.getDB(StaticDatabases.GLOBAL.name)
@ -18,7 +29,7 @@ async function authenticate(issuer, sub, profile, jwtClaims, accessToken, refres
const userId = generateGlobalUserID(profile.id) const userId = generateGlobalUserID(profile.id)
try { try {
// use the google profile id // use the OIDC profile id
dbUser = await db.get(userId) dbUser = await db.get(userId)
} catch (err) { } catch (err) {
const user = { const user = {
@ -28,13 +39,13 @@ async function authenticate(issuer, sub, profile, jwtClaims, accessToken, refres
...profile._json, ...profile._json,
} }
// check if an account with the google email address exists locally // check if an account with the OIDC email address exists locally
const users = await db.query(`database/${ViewNames.USER_BY_EMAIL}`, { const users = await db.query(`database/${ViewNames.USER_BY_EMAIL}`, {
key: profile._json.email, key: profile._json.email,
include_docs: true, include_docs: true,
}) })
// Google user already exists by email // OIDC user already exists by email
if (users.rows.length > 0) { if (users.rows.length > 0) {
const existing = users.rows[0].doc const existing = users.rows[0].doc
@ -74,36 +85,41 @@ async function authenticate(issuer, sub, profile, jwtClaims, accessToken, refres
} }
/** /**
* Create an instance of the google passport strategy. This wrapper fetches the configuration * Create an instance of the oidc passport strategy. This wrapper fetches the configuration
* from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport. * from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport.
* @returns Dynamically configured Passport Google Strategy * @returns Dynamically configured Passport OIDC Strategy
*/ */
exports.strategyFactory = async function () { exports.strategyFactory = async function (callbackUrl) {
try { try {
const configurationUrl =
"https://login.microsoftonline.com/2668c0dd-7ed2-4db3-b387-05b6f9204a70/v2.0/.well-known/openid-configuration"
const clientSecret = "g-ty~2iW.bo.88xj_QI6~hdc-H8mP2Xbnd"
const clientId = "bed2017b-2f53-42a9-8ef9-e58918935e07"
/* if (!clientId || !clientSecret || !callbackUrl || !configurationUrl) {
const { clientID, clientSecret, callbackURL } = config
if (!clientID || !clientSecret || !callbackURL) {
throw new Error( throw new Error(
"Configuration invalid. Must contain google clientID, clientSecret and callbackURL" "Configuration invalid. Must contain clientID, clientSecret, callbackUrl and configurationUrl"
) )
} }
*/
return new OIDCStrategy( const response = await fetch(configurationUrl)
{ if (response.ok) {
issuer: "https://base.uri/auth/realms/realm_name", const body = await response.json()
authorizationURL: "https://base.uri/auth/realms/realm_name/protocol/openid-connect/auth",
tokenURL: "https://base.uri/auth/realms/realm_name/protocol/openid-connect/token", return new OIDCStrategy(
userInfoURL: "https://base.uri/auth/realms/realm_name/protocol/openid-connect/userinfo", {
clientID: "my_client_id", issuer: body.issuer,
clientSecret: "my_client_secret", authorizationURL: body.authorization_endpoint,
callbackURL: "http://localhost:10000/api/admin/auth/oidc/callback", tokenURL: body.token_endpoint,
scope: "openid profile email", userInfoURL: body.userinfo_endpoint,
}, clientID: clientId,
authenticate clientSecret: clientSecret,
) callbackURL: callbackUrl,
scope: "profile email",
},
authenticate
)
}
} catch (err) { } catch (err) {
console.error(err) console.error(err)
throw new Error("Error constructing OIDC authentication strategy", err) throw new Error("Error constructing OIDC authentication strategy", err)

View File

@ -1,15 +1,12 @@
<script> <script>
import { ActionButton } from "@budibase/bbui" import { ActionButton } from "@budibase/bbui"
import { admin } from "stores/portal" // import { admin } from "stores/portal"
let show = true let show = true
</script> </script>
{#if show} {#if show}
<ActionButton <ActionButton on:click={() => window.open("/api/admin/auth/oidc", "_blank")}>
on:click={() => window.open("/api/admin/auth/oidc", "_blank")}
>
<div class="inner"> <div class="inner">
<p>Sign in with OIDC</p> <p>Sign in with OIDC</p>
</div> </div>
@ -25,10 +22,10 @@
padding-top: var(--spacing-xs); padding-top: var(--spacing-xs);
padding-bottom: var(--spacing-xs); padding-bottom: var(--spacing-xs);
} }
.inner img { /* .inner img {
width: 18px; width: 18px;
margin: 3px 10px 3px 3px; margin: 3px 10px 3px 3px;
} } */
.inner p { .inner p {
margin: 0; margin: 0;
} }

View File

@ -131,10 +131,17 @@ exports.googleAuth = async (ctx, next) => {
)(ctx, next) )(ctx, next)
} }
// Minimal OIDC attempt async function oidcStrategyFactory(ctx) {
const callbackUrl = `${ctx.protocol}://${ctx.host}/api/admin/auth/oidc/callback`
return oidc.strategyFactory(callbackUrl)
}
/**
* The initial call that OIDC authentication makes to take you to the configured OIDC login screen.
* On a successful login, you will be redirected to the oidcAuth callback route.
*/
exports.oidcPreAuth = async (ctx, next) => { exports.oidcPreAuth = async (ctx, next) => {
const strategy = await oidc.strategyFactory() const strategy = await oidcStrategyFactory(ctx)
return passport.authenticate(strategy, { return passport.authenticate(strategy, {
scope: ["profile", "email"], scope: ["profile", "email"],
@ -142,7 +149,7 @@ exports.oidcPreAuth = async (ctx, next) => {
} }
exports.oidcAuth = async (ctx, next) => { exports.oidcAuth = async (ctx, next) => {
const strategy = await oidc.strategyFactory() const strategy = await oidcStrategyFactory(ctx)
return passport.authenticate( return passport.authenticate(
strategy, strategy,

View File

@ -14,7 +14,7 @@ const redis = require("./utilities/redis")
const app = new Koa() const app = new Koa()
app.keys = ['secret', 'key']; app.keys = ["secret", "key"]
// set up top level koa middleware // set up top level koa middleware
app.use(koaBody({ multipart: true })) app.use(koaBody({ multipart: true }))