-
Notifications
You must be signed in to change notification settings - Fork 0
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
1 parent
9d5f66d
commit 6c9bfcc
Showing
15 changed files
with
277 additions
and
57 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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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,2 @@ | ||
VITE_OIDC_ISSUER="https://testid.cerberauth.com" | ||
VITE_OIDC_CLIENT_ID="" |
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,30 @@ | ||
FROM node:lts as development | ||
|
||
WORKDIR /app | ||
|
||
COPY package*.json ./ | ||
RUN npm ci | ||
|
||
COPY . . | ||
|
||
ENV CI=true | ||
ENV PORT=3000 | ||
|
||
CMD [ "npm", "start" ] | ||
|
||
FROM development as builder | ||
|
||
RUN npm run build | ||
|
||
FROM nginx:alpine | ||
|
||
# Copy config nginx | ||
COPY --from=builder /app/.nginx/nginx.conf /etc/nginx/conf.d/default.conf | ||
|
||
WORKDIR /usr/share/nginx/html | ||
|
||
# Remove default nginx static assets | ||
RUN rm -rf ./* | ||
|
||
# Copy static assets from builder stage | ||
COPY --from=builder /app/dist . |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
Empty file.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
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,49 @@ | ||
import { reactive, computed } from 'vue' | ||
import { | ||
login as oidcLogin, | ||
handleLoginRedirect as oidcHandleLoginRedirect, | ||
logout as oidcLogout, | ||
} from './oidc' | ||
|
||
interface AuthState { | ||
isAuthenticated: boolean | ||
user: null | { [key: string]: any } | ||
} | ||
|
||
const state = reactive<AuthState>({ | ||
isAuthenticated: false, | ||
user: null, | ||
}) | ||
|
||
export function useAuth() { | ||
const login = () => { | ||
oidcLogin() | ||
} | ||
|
||
const handleLoginRedirect = async () => { | ||
const _user = await oidcHandleLoginRedirect() | ||
if (!_user) { | ||
return | ||
} | ||
|
||
state.isAuthenticated = true | ||
state.user = _user | ||
} | ||
|
||
const logout = () => { | ||
state.isAuthenticated = false | ||
state.user = null | ||
oidcLogout() | ||
} | ||
|
||
const isAuthenticated = computed(() => state.isAuthenticated) | ||
const user = computed(() => state.user) | ||
|
||
return { | ||
login, | ||
handleLoginRedirect, | ||
logout, | ||
isAuthenticated, | ||
user, | ||
} | ||
} |
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,156 @@ | ||
import * as oauth from 'oauth4webapi' | ||
|
||
const webStorageKey = 'oidc:auth' | ||
let accessToken: string | undefined | ||
let idToken: string | undefined | ||
|
||
if (!import.meta.env.VITE_OIDC_ISSUER) { | ||
throw new Error('OIDC_ISSUER is not set') | ||
} | ||
|
||
if (!import.meta.env.VITE_OIDC_CLIENT_ID) { | ||
throw new Error('OIDC_CLIENT_ID is not set') | ||
} | ||
|
||
const issuer = import.meta.env.VITE_OIDC_ISSUER | ||
const clientId = import.meta.env.VITE_OIDC_CLIENT_ID | ||
|
||
const issuerUrl = new URL(issuer) | ||
|
||
const client: oauth.Client = { | ||
client_id: clientId, | ||
redirect_uris: [window.location.origin], | ||
} | ||
const clientAuth = oauth.None() | ||
|
||
let _as: oauth.AuthorizationServer | undefined | ||
const getAuthorizationServer = async (): Promise<oauth.AuthorizationServer> => { | ||
if (_as) { | ||
return _as | ||
} | ||
|
||
_as = await oauth.discoveryRequest(issuerUrl, { algorithm: 'oidc' }) | ||
.then((response) => oauth.processDiscoveryResponse(issuerUrl, response)) | ||
if (!_as) { | ||
throw new Error('Invalid Authorization Server') | ||
} | ||
|
||
return _as | ||
} | ||
|
||
type LoginParams = { | ||
scope?: string; | ||
redirectUri?: string; | ||
} | ||
|
||
export const login = async (params?: LoginParams) => { | ||
const as = await getAuthorizationServer() | ||
|
||
const scope = params?.scope || 'openid email' | ||
let redirectUri = params?.redirectUri | ||
if (!redirectUri && Array.isArray(client.redirect_uris) && client.redirect_uris.length > 1) { | ||
redirectUri = client.redirect_uris[0]?.toString() | ||
} | ||
redirectUri = redirectUri || window.location.origin | ||
|
||
const code_challenge_method = 'S256' | ||
/** | ||
* The following MUST be generated for every redirect to the authorization_endpoint. You must store | ||
* the code_verifier and nonce in the end-user session such that it can be recovered as the user | ||
* gets redirected from the authorization server back to your application. | ||
*/ | ||
const codeVerifier = oauth.generateRandomCodeVerifier() | ||
const codeChallenge = await oauth.calculatePKCECodeChallenge(codeVerifier) | ||
let nonce: string | undefined | ||
|
||
const authorizationUrl = new URL(as.authorization_endpoint!) | ||
authorizationUrl.searchParams.set('client_id', client.client_id) | ||
authorizationUrl.searchParams.set('redirect_uri', redirectUri) | ||
authorizationUrl.searchParams.set('response_type', 'code') | ||
authorizationUrl.searchParams.set('scope', scope) | ||
authorizationUrl.searchParams.set('code_challenge', codeChallenge) | ||
authorizationUrl.searchParams.set('code_challenge_method', code_challenge_method) | ||
|
||
let state: string | undefined | ||
state = oauth.generateRandomState() | ||
authorizationUrl.searchParams.set('state', state) | ||
|
||
/** | ||
* We cannot be sure the AS supports PKCE so we're going to use nonce too. Use of PKCE is | ||
* backwards compatible even if the AS doesn't support it which is why we're using it regardless. | ||
*/ | ||
if (as.code_challenge_methods_supported?.includes('S256') !== true) { | ||
nonce = oauth.generateRandomNonce() | ||
authorizationUrl.searchParams.set('nonce', nonce) | ||
} | ||
|
||
console.log('store code_verifier and nonce in the end-user session') | ||
sessionStorage.setItem(webStorageKey, JSON.stringify({ codeVerifier, state, nonce, redirectUri })) | ||
|
||
console.log('Redirecting to Authorization Server', authorizationUrl.toString()) | ||
window.location.assign(authorizationUrl.toString()) | ||
} | ||
|
||
export const handleLoginRedirect = async (): Promise<oauth.UserInfoResponse | undefined> => { | ||
// @ts-expect-error | ||
const currentUrl: URL = new URL(window.location) | ||
if (!currentUrl.searchParams.has('code')) { | ||
return undefined | ||
} | ||
|
||
const as = await getAuthorizationServer() | ||
|
||
const storage = sessionStorage.getItem(webStorageKey) | ||
if (!storage) { | ||
throw new Error('No stored codeVerifier and nonce found') | ||
} | ||
sessionStorage.removeItem(webStorageKey) | ||
const { codeVerifier, state, nonce, redirectUri } = JSON.parse(storage) | ||
|
||
const params = oauth.validateAuthResponse(as, client, currentUrl, state) | ||
|
||
const authorizationResponse = await oauth.authorizationCodeGrantRequest( | ||
as, | ||
client, | ||
clientAuth, | ||
params, | ||
redirectUri, | ||
codeVerifier, | ||
) | ||
|
||
const authorizationCodeResult = await oauth.processAuthorizationCodeResponse(as, client, authorizationResponse, { expectedNonce: nonce }) | ||
|
||
console.log('Access Token Response', authorizationCodeResult) | ||
accessToken = authorizationCodeResult.access_token | ||
idToken = authorizationCodeResult.id_token | ||
const claims = oauth.getValidatedIdTokenClaims(authorizationCodeResult) | ||
console.log('ID Token Claims', claims) | ||
|
||
const sub = claims?.sub | ||
if (!sub) { | ||
throw new Error('No sub claim in ID Token') | ||
} | ||
|
||
// UserInfo Request | ||
const response = await oauth.userInfoRequest(as, client, accessToken) | ||
const user = await oauth.processUserInfoResponse(as, client, sub, response) | ||
console.log('UserInfo Response', user) | ||
|
||
window.history.replaceState({}, document.title, redirectUri || window.location.origin) | ||
return user | ||
} | ||
|
||
export const logout = async () => { | ||
if (!idToken) { | ||
throw new Error('No ID Token') | ||
} | ||
|
||
const as = await getAuthorizationServer() | ||
|
||
const endSessionUrl = new URL(as.end_session_endpoint!) | ||
endSessionUrl.searchParams.set('post_logout_redirect_uri', window.location.origin) | ||
endSessionUrl.searchParams.set('id_token_hint', idToken) | ||
console.log('Redirecting to End Session Endpoint', endSessionUrl.toString()) | ||
|
||
window.location.assign(endSessionUrl.toString()) | ||
} |
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,25 +1,10 @@ | ||
import './assets/main.css' | ||
|
||
import { createApp } from 'vue' | ||
import { createAuth } from 'vue-auth3' | ||
import driverBearerToken from 'vue-auth3/drivers/auth/bearer-token' | ||
import driverHttpFetch from 'vue-auth3/drivers/http/fetch' | ||
import App from './App.vue' | ||
import './drivers/testid' | ||
import router from './router' | ||
|
||
const auth = createAuth({ | ||
plugins: { | ||
router, | ||
}, | ||
drivers: { | ||
auth: driverBearerToken, | ||
http: driverHttpFetch, | ||
}, | ||
}) | ||
|
||
const app = createApp(App) | ||
|
||
app.use(router).use(auth) | ||
// app.use(router) | ||
app.use(router) | ||
app.mount('#app') |
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
Oops, something went wrong.