This repository has been archived on 2022-08-14. You can view files and clone it, but cannot push or open issues or pull requests.
outline/server/routes/auth/providers/oidc.js

139 lines
4.2 KiB
JavaScript

// @flow
import passport from "@outlinewiki/koa-passport";
import fetch from "fetch-with-proxy";
import Router from "koa-router";
import get from "lodash/get";
import { Strategy } from "passport-oauth2";
import accountProvisioner from "../../../commands/accountProvisioner";
import env from "../../../env";
import {
OIDCMalformedUserInfoError,
AuthenticationError,
} from "../../../errors";
import passportMiddleware from "../../../middlewares/passport";
import { getAllowedDomains } from "../../../utils/authentication";
import { StateStore } from "../../../utils/passport";
const router = new Router();
const providerName = "oidc";
const OIDC_DISPLAY_NAME = process.env.OIDC_DISPLAY_NAME || "OpenID Connect";
const OIDC_CLIENT_ID = process.env.OIDC_CLIENT_ID;
const OIDC_CLIENT_SECRET = process.env.OIDC_CLIENT_SECRET;
const OIDC_AUTH_URI = process.env.OIDC_AUTH_URI;
const OIDC_TOKEN_URI = process.env.OIDC_TOKEN_URI;
const OIDC_USERINFO_URI = process.env.OIDC_USERINFO_URI;
const OIDC_SCOPES = process.env.OIDC_SCOPES || "";
const OIDC_USERNAME_CLAIM =
process.env.OIDC_USERNAME_CLAIM || "preferred_username";
const allowedDomains = getAllowedDomains();
export const config = {
name: OIDC_DISPLAY_NAME,
enabled: !!OIDC_CLIENT_ID,
};
const scopes = OIDC_SCOPES.split(" ");
Strategy.prototype.userProfile = async function (accessToken, done) {
try {
const response = await fetch(OIDC_USERINFO_URI, {
credentials: "same-origin",
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
try {
return done(null, await response.json());
} catch (err) {
return done(err);
}
} catch (err) {
return done(err);
}
};
if (OIDC_CLIENT_ID) {
passport.use(
providerName,
new Strategy(
{
authorizationURL: OIDC_AUTH_URI,
tokenURL: OIDC_TOKEN_URI,
clientID: OIDC_CLIENT_ID,
clientSecret: OIDC_CLIENT_SECRET,
callbackURL: `${env.URL}/auth/${providerName}.callback`,
passReqToCallback: true,
scope: OIDC_SCOPES,
store: new StateStore(),
state: true,
pkce: false,
},
// OpenID Connect standard profile claims can be found in the official
// specification.
// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
// Non-standard claims may be configured by individual identity providers.
// Any claim supplied in response to the userinfo request will be
// available on the `profile` parameter
async function (req, accessToken, refreshToken, profile, done) {
try {
const parts = profile.email.split("@");
const domain = parts.length && parts[1];
if (!domain) {
throw new OIDCMalformedUserInfoError();
}
if (allowedDomains.length && !allowedDomains.includes(domain)) {
throw new AuthenticationError(
`Domain ${domain} is not on the whitelist`
);
}
const subdomain = domain.split(".")[0];
const result = await accountProvisioner({
ip: req.ip,
team: {
// https://github.com/outline/outline/pull/2388#discussion_r681120223
name: "Wiki",
domain,
subdomain,
},
user: {
name: profile.name,
email: profile.email,
avatarUrl: profile.picture,
// Claim name can be overriden using an env variable.
// Default is 'preferred_username' as per OIDC spec.
username: get(profile, OIDC_USERNAME_CLAIM),
},
authenticationProvider: {
name: providerName,
providerId: domain,
},
authentication: {
providerId: profile.sub,
accessToken,
refreshToken,
scopes,
},
});
return done(null, result.user, result);
} catch (err) {
return done(err, null);
}
}
)
);
router.get(providerName, passport.authenticate(providerName));
router.get(`${providerName}.callback`, passportMiddleware(providerName));
}
export default router;