I am currently building out a Vue3 SPA and trying to use the oidc-client-ts javascript library for authenticating a user. Up to this point, I have been successful at implementing the login/logout functionality with my existing code. The problem I am experiencing is that I am not able to hook into the addUserLoaded or addUserSignedIn events that the UserManager provides (https://authts.github.io/oidc-client-ts/classes/UserManagerEvents.html).
One point to note is that when you land on my domain, we'll say myapp.mydomain.com, you are redirected to our identity server at myid.mydomain.com and then redirected back once login in successful.
On startup I've got the following
fetch("config.json", {'method': "GET"})
.then(response => {
if(response.ok) {
response.json().then(appConfigData => {
window.appConfig = appConfigData;
createApp(App)
.use(store)
.use(router)
.use(AccordionPlugin)
.component("font-awesome-icon", FontAwesomeIcon)
.mount("#app");
});
} else{
alert("Server returned " + response.status + " : " + response.statusText);
}
});
This window.appConfig has the settings object for initializing the UserManager
This is my AuthService.ts class which I export. Note the events in the constructor that I am registering to the UserManager.
import { UserManager, WebStorageStateStore, User, UserManagerEvents } from "oidc-client-ts";
import jwt_decode from "jwt-decode";
import store from "#/store";
export default class AuthService {
private userManager: UserManager;
constructor(configuration: any) {
const IDP_URL: string = configuration.IDP_URL;
const REDIRECT_URI: string = configuration.REDIRECT_URI;
const AUTH_TOKEN_URI: string = configuration.AUTH_TOKEN_URI;
const CLIENT_ID: string = configuration.CLIENT_ID;
const SILENT_RENEW_REDIRECT_URI: string = configuration.SILENT_RENEW_REDIRECT_URI;
const POPUP_REDIRECT_URI: string = configuration.POPUP_REDIRECT_URI;
const settings: any = {
userStore: new WebStorageStateStore({ store: window.localStorage }),
authority: IDP_URL,
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
popup_redirect_uri: POPUP_REDIRECT_URI,
response_type: "code",
automaticSilentRenew: true,
silent_redirect_uri: SILENT_RENEW_REDIRECT_URI,
scope: "openid profile ContactCenterApi.READ_WRITE",
post_logout_redirect_uri: AUTH_TOKEN_URI,
loadUserInfo: true
};
this.userManager = new UserManager(settings);
this.userManager.events.addUserLoaded(_args => {
console.log("USER LOADED EVENT");
debugger;
this.userManager.getUser().then(usr => {
store.dispatch("getProducts", usr?.profile.sub) // load the users products
});
});
this.userManager.events.addUserSignedIn(() => {
console.log("USER SIGNED IN EVENT");
debugger;
this.userManager.getUser().then(usr => {
store.dispatch("getProducts", usr?.profile.sub) // load the users products
});
});
}
public getUser(): Promise<User|null> {
return this.userManager.getUser();
}
public async login(): Promise<void> {
return this.userManager.signinRedirect();
}
public logout(): Promise<void> {
return this.userManager.signoutRedirect();
}
In my router/index.ts file I have the following
const auth = new AuthService(window.appConfig);
router.beforeEach(async (to, from, next) => {
const user = await auth.getUser();
const baseUri = window.appConfig.BASE_URI + "/#";
console.log("redirectedUri = " + baseUri + to.fullPath);
if (user) {
if (!user.expired) {
next();
} else {
sessionStorage.setItem("redirectedUri", baseUri + to.fullPath);
await auth.login();
}
} else {
sessionStorage.setItem("redirectedUri", baseUri + to.fullPath);
await auth.login();
}
});
export default router;
My logout button is pretty basic
import AuthService from "#/auth/AuthService";
onselect: async function(event: MenuEventArgs) {
var authService = new AuthService(window.appConfig); // settings are stored in the window when the app mounts
if(event.item.text === 'Logout') {
await authService.logout();
}
}
For example, one of the events that I would like to hook into is the addUserSignedIn event. However even when logging out and logging back in, the event does not fire. I would like to use this event to do some basic initialization of the user data. Any ideas on why these events are not firing?
Here is what ended up working for me. This is my AuthService.ts class that gets used around the codebase.
// AuthService.ts
import { UserManager, WebStorageStateStore, User } from "oidc-client-ts";
import jwt_decode from "jwt-decode";
import store from "#/store";
let userManager: UserManager;
const currentURL = document.location.origin;
fetch(currentURL + "/config.json", { 'method': "GET" })
.then(config => {
config.json()
.then(configuration => {
configuration['SSO_SETTINGS']['userStore'] = new WebStorageStateStore({ store: window.localStorage });
userManager = new UserManager(configuration['SSO_SETTINGS']);
userManager.events.addUserLoaded(newUser => {
console.log("USER LOADED EVENT");
userManager.storeUser(newUser);
userManager.getUser().then(usr => {
store.dispatch("getUserInfo", usr?.profile.sub) // load the user from my api
});
});
userManager.events.addAccessTokenExpired(() => {
userManager.signoutRedirect();
})
userManager.events.addSilentRenewError(error => {
console.log("ERROR RENEWING ACCESS TOKEN.");
console.log(error);
})
})
})
export default class AuthService {
public static getUser(): Promise<User | null> {
return userManager.getUser();
}
public static async login(): Promise<void> {
return userManager.signinRedirect();
}
public static logout(): Promise<void> {
localStorage.clear();
sessionStorage.clear();
return userManager.signoutRedirect();
}
}
My updated SSO settings object in my config.json that gets passed to the user manager
"SSO_SETTINGS": {
"userStore": "",
"authority": "https://myauthorityurl.com",
"client_id": "MyClientName",
"redirect_uri": "http://localhost:8080/callback.html",
"response_type": "code",
"response_mode": "query",
"automaticSilentRenew": true,
"monitorSession": true,
"silent_redirect_uri": "http://localhost:8080/silent-renew.html",
"scope": "openid profile",
"post_logout_redirect_uri": "http://localhost:8080",
"loadUserInfo": true,
"metadata": {
"issuer": "https://myauthorityurl.com",
"jwks_uri": "https://myauthorityurl.com/.well-known/openid-configuration/jwks",
"authorization_endpoint": "https://myauthorityurl.com/connect/authorize",
"end_session_endpoint": "https://myauthorityurl/Account/Logout",
"token_endpoint": "https://iddev02.carexm.com/connect/token",
"userinfo_endpoint": "https://iddev02.carexm.com/connect/userinfo"
}
},
And for the sake of completeness, here are my callback.html and silent-redirect.html pages. These two files are placed in the top level public folder along with your config.json and oidc-client-ts.min.js file.
callback.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Waiting...</title>
</head>
<body>
<script src="oidc-client-ts.min.js"></script>
<script>
var config = fetch("config.json", {'method': "GET"})
.then(config =>
{
config.json().then(settings => {
var mgr = new oidc.UserManager(settings['SSO_SETTINGS']);
mgr.settings.userStore = new oidc.WebStorageStateStore({ store: window.localStorage });
mgr.signinRedirectCallback()
.then(() =>
{
window.location.href = sessionStorage.getItem("redirectedUri") ?? "../";
})
})
});
</script>
</body>
</html>
silent-renew.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Waiting...</title>
</head>
<body>
<script src="oidc-client-ts.min.js"></script>
<script>
var config = fetch("config.json", {'method': "GET"})
.then(config =>
{
config.json().then(settings => {
var mgr = new oidc.UserManager(settings['SSO_SETTINGS']).signinSilentCallback();
})
});
</script>
</body>
</html>
Related
I am trying to connect my vue js application to a msal b2c backend the login is done but now i need to make a button for password reset and sign up i cant find a sign up popup anywhere and i dont know how to set up a password reset popup.
When i click the password reset link in the login popup the popup closes and nothing happens after that
The way i currently have it set up is:
LoginView.vue
<template>
<div class="grid grid-cols-12">
<div class="default-container">
<p>u bent nog niet ingelogd log nu in</p>
<button type="button" #click="login">Login</button>
</div>
</div>
</template>
<script setup>
import useAuthStore from '../stores/AuthStore';
function login() {
useAuthStore().login();
}
</script>
Authstore.js
import { defineStore } from 'pinia';
import AuthService from '../services/AuthService';
const clientId = import.meta.env.VITE_APP_CLIENT_ID;
const authority = import.meta.env.VITE_APP_AUTHORITY;
const scopes = [import.meta.env.VITE_APP_SCOPE_READ];
const authService = new AuthService(clientId, authority, scopes);
const useAuthStore = defineStore('AuthStore', {
state: () => ({
isAuthenticated: !!localStorage.getItem('apiToken'),
apiToken: localStorage.getItem('apiToken'),
user: JSON.parse(localStorage.getItem('userDetails')),
error: null,
}),
getters: {
isLoggedIn: (state) => state.isAuthenticated,
currentApiToken: (state) => state.apiToken,
currentUser: (state) => state.user,
currentError: (state) => state.error,
},
actions: {
async login() {
try {
const token = await authService.login();
this.apiToken = token.apiToken;
localStorage.setItem('apiToken', token.apiToken);
this.isAuthenticated = true;
fetch(`${import.meta.env.VITE_APP_API_URL}/account/current`, {
headers: {
Authorization: `Bearer ${token.apiToken}`,
},
})
.then((response) => response.json())
.then((data) => {
localStorage.setItem('userDetails', JSON.stringify(data));
this.user = data;
});
} catch (error) {
this.error = error;
}
},
async logout() {
try {
await authService.logout();
this.user = null;
this.apiToken = null;
this.isAuthenticated = false;
localStorage.removeItem('apiToken');
localStorage.removeItem('userDetails');
} catch (error) {
this.error = error;
}
},
},
});
export default useAuthStore;
AuthService.js
import * as Msal from 'msal';
export default class AuthService {
constructor(clientId, authority, scopes) {
this.app = new Msal.UserAgentApplication({
auth: {
clientId,
authority,
postLogoutRedirectUri: window.location.origin,
redirectUri: window.location.origin,
validateAuthority: false,
},
cache: {
cacheLocation: 'localStorage',
},
});
this.scopes = scopes;
}
async login() {
const loginRequest = {
scopes: this.scopes,
prompt: 'select_account',
};
const accessTokenRequest = {
scopes: this.scopes,
};
let token = {};
try {
await this.app.loginPopup(loginRequest);
} catch (error) {
return undefined;
}
try {
const acquireTokenSilent = await this.app.acquireTokenSilent(accessTokenRequest);
token = {
apiToken: acquireTokenSilent.accessToken,
expiresOn: acquireTokenSilent.expiresOn,
};
} catch (error) {
try {
const acquireTokenPopup = await this.app.acquireTokenPopup(accessTokenRequest);
token = {
apiToken: acquireTokenPopup.accessToken,
expiresOn: acquireTokenPopup.expiresOn,
};
} catch (errorPopup) {
return undefined;
}
}
return token;
}
logout() {
this.app.logout();
}
}
How do I mock authenticate with Nuxt and Cypress?
I have a FastAPI backend that issues a JWT to a frontend NuxtJS application. I want to test the frontend using Cypress. I am battling to mock authenticate.
Here is a simple Cypress test:
// cypress/e2e/user_authentication_test.cy.js
describe("A User logging in", () => {
it.only("can login by supplying the correct credentials", () => {
cy.mockLogin().then(() => {
cy.visit(`${Cypress.env("BASE_URL")}/dashboard`)
.window()
.its("$nuxt.$auth")
.its("loggedIn")
.should("equal", true);
});
});
});
The test above fails at the should assertion, and the user is not redirected.
The mockLogin command is defined as:
// cypress/support/commands.js
Cypress.Commands.add(
'mockLogin',
(username = 'someone', password = 'my_secret_password_123') => {
cy.intercept('POST', 'http://localhost:5000/api/v1/auth/token', {
fixture: 'auth/valid_auth_token.json',
}).as('token_mock')
cy.visit(`${Cypress.env('BASE_URL')}/login`)
cy.get('#login-username').type(username)
cy.get('#login-password').type(`${password}{enter}`)
cy.wait('#token_mock')
}
)
Where valid_auth_token.json contains a JWT.
The actual login is done as follows:
<!-- components/auth/LoginForm.vue -->
<template>
<!-- Login form goes here -->
</template>
<script>
import jwt_decode from 'jwt-decode' // eslint-disable-line camelcase
export default {
name: 'LoginForm',
data() {
return {
username: '',
password: '',
}
},
methods: {
async login() {
const formData = new FormData()
formData.append('username', this.username)
formData.append('password', this.password)
try {
await this.$auth
.loginWith('cookie', {
data: formData,
})
.then((res) => {
const decode = jwt_decode(res.data.access_token) // eslint-disable-line camelcase
this.$auth.setUser(decode)
this.$router.push('/')
})
} catch (error) {
// error handling
}
},
},
}
</script>
I cannot correctly set my jwt token from my cookie to my Headers for an authenticaed gql request using apollo client.
I believe the problem is on my withApollo.js file, the one that wraps the App component on _app.js. The format of this file is based off of the wes bos advanced react nextjs graphql course. What happens is that nextauth saves the JWT as a cookie, and I can then grab the JWT from that cookie using a custom regex function. Then I try to set this token value to the authorization bearer header. The problem is that on the first load of a page with a gql query needing a jwt token, I get the error "Cannot read property 'cookie' of undefined". But, if I hit browser refresh, then suddenly it works and the token was successfully set to the header.
Some research led me to adding a setcontext link and so that's where I try to perform this operation. I tried to async await setting the token value but that doesn't seem to have helped. It just seems like the headers don't want to get set until on the refresh.
lib/withData.js
import { ApolloClient, ApolloLink, InMemoryCache } from '#apollo/client';
import { onError } from '#apollo/link-error';
import { getDataFromTree } from '#apollo/react-ssr';
import { createUploadLink } from 'apollo-upload-client';
import withApollo from 'next-with-apollo';
import { setContext } from 'apollo-link-context';
import { endpoint, prodEndpoint } from '../config';
import paginationField from './paginationField';
const getCookieValue = (name, cookie) =>
cookie.match(`(^|;)\\s*${name}\\s*=\\s*([^;]+)`)?.pop() || '';
let token;
function createClient(props) {
const { initialState, headers, ctx } = props;
console.log({ headers });
// console.log({ ctx });
return new ApolloClient({
link: ApolloLink.from([
onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors)
graphQLErrors.forEach(({ message, locations, path }) =>
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
)
);
if (networkError)
console.log(
`[Network error]: ${networkError}. Backend is unreachable. Is it running?`
);
}),
setContext(async (request, previousContext) => {
token = await getCookieValue('token', headers.cookie);
return {
headers: {
authorization: token ? `Bearer ${token}` : '',
},
};
}),
createUploadLink({
uri: process.env.NODE_ENV === 'development' ? endpoint : prodEndpoint,
fetchOptions: {
credentials: 'include',
},
headers,
}),
]),
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
// TODO: We will add this together!
// allProducts: paginationField(),
},
},
},
}).restore(initialState || {}),
});
}
export default withApollo(createClient, { getDataFromTree });
page/_app.js
import { ApolloProvider } from '#apollo/client';
import NProgress from 'nprogress';
import Router from 'next/router';
import { Provider, getSession } from 'next-auth/client';
import { CookiesProvider } from 'react-cookie';
import nookies, { parseCookies } from 'nookies';
import Page from '../components/Page';
import '../components/styles/nprogress.css';
import withData from '../lib/withData';
Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done());
function MyApp({ Component, pageProps, apollo, user }) {
return (
<Provider session={pageProps.session}>
<ApolloProvider client={apollo}>
<Page>
<Component {...pageProps} {...user} />
</Page>
</ApolloProvider>
</Provider>
);
}
MyApp.getInitialProps = async function ({ Component, ctx }) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
pageProps.query = ctx.query;
const user = {};
const { req } = ctx;
const session = await getSession({ req });
if (session) {
user.email = session.user.email;
user.id = session.user.id;
user.isUser = !!session;
// Set
nookies.set(ctx, 'token', session.accessToken, {
maxAge: 30 * 24 * 60 * 60,
path: '/',
});
}
return {
pageProps,
user: user || null,
};
};
export default withData(MyApp);
api/auth/[...nextAuth.js]
import NextAuth from 'next-auth';
import Providers from 'next-auth/providers';
import axios from 'axios';
const providers = [
Providers.Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
Providers.Credentials({
name: 'Credentials',
credentials: {
username: { label: 'Username', type: 'text', placeholder: 'jsmith' },
password: { label: 'Password', type: 'password' },
},
authorize: async (credentials) => {
const user = await axios
.post('http://localhost:1337/auth/local', {
identifier: credentials.username,
password: credentials.password,
})
.then((res) => {
res.data.user.token = res.data.jwt;
return res.data.user;
}) // define user as res.data.user (will be referenced in callbacks)
.catch((error) => {
console.log('An error occurred:', error);
});
if (user) {
return user;
}
return null;
},
}),
];
const callbacks = {
// Getting the JWT token from API response
async jwt(token, user, account, profile, isNewUser) {
// WRITE TO TOKEN (from above sources)
if (user) {
const provider = account.provider || user.provider || null;
let response;
let data;
switch (provider) {
case 'google':
response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/auth/google/callback?access_token=${account?.accessToken}`
);
data = await response.json();
if (data) {
token.accessToken = data.jwt;
token.id = data.user._id;
} else {
console.log('ERROR No data');
}
break;
case 'local':
response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/auth/local/callback?access_token=${account?.accessToken}`
);
data = await response.json();
token.accessToken = user.token;
token.id = user.id;
break;
default:
console.log(`ERROR: Provider value is ${provider}`);
break;
}
}
return token;
},
async session(session, token) {
// WRITE TO SESSION (from token)
// console.log(token);
session.accessToken = token.accessToken;
session.user.id = token.id;
return session;
},
redirect: async (url, baseUrl) => baseUrl,
};
const sessionPreferences = {
session: {
jwt: true,
},
};
const options = {
providers,
callbacks,
sessionPreferences,
};
export default (req, res) => NextAuth(req, res, options);
I am trying to pass the name of the user after authentication into a Vue component, but I get a name: undefined value after load.
Here is my AuthService.js:
//config details taken from OAUTH JS doc: https://github.com/andreassolberg/jso
import { JSO, Fetcher } from 'jso';
const client = new JSO({
providerID: '<my-provider>',
default_lifetime: 1800,
client_id: '<my-client-id>',
redirect_uri: 'http://localhost:8080/',
authorization:'<my-auth-server>/oauth/authorize'
//scopes: { request: ['https://www.googleapis.com/auth/userinfo.profile'] }
});
export default {
getProfile() {
// JSO plugin provides a simple wrapper around the fetch API to handle headers
let f = new Fetcher(client);
let url = 'https://www.googleapis.com/auth/userinfo.profile';
f.fetch(url, {})
.then(data => {
return data.json();
})
.then(data => {
return data.user_name;
})
.catch(err => {
console.error('Error from fetcher', err);
});
}
};
Then, in my single file component named MainNav, I have:
import AuthService from "#/AuthService";
export default {
name: "MainNav",
data() {
return {
name: ""
};
},
created() {
this.name = AuthService.getProfile();
}
};
</script>
Anyone have any tips on how I can get the user_name value from the AuthService to my component? I will then need to then display the name in my nav template. Doing a console.log test works fine, just can't return it to my SFC. Also, the JSO library is here: https://github.com/andreassolberg/jso#fetching-data-from-a-oauth-protected-endpoint
Because getProfile returns nothing (undefined). I see you use es6 then you can use async functions
//config details taken from OAUTH JS doc: https://github.com/andreassolberg/jso
import { JSO, Fetcher } from 'jso';
const client = new JSO({
providerID: '<my-provider>',
default_lifetime: 1800,
client_id: '<my-client-id>',
redirect_uri: 'http://localhost:8080/',
authorization:'<my-auth-server>/oauth/authorize'
//scopes: { request: ['https://www.googleapis.com/auth/userinfo.profile'] }
});
export default {
getProfile() {
// JSO plugin provides a simple wrapper around the fetch API to handle headers
let f = new Fetcher(client);
let url = 'https://www.googleapis.com/auth/userinfo.profile';
return f.fetch(url, {}) // return promise here
.then(data => {
return data.json();
})
.then(data => {
return data.user_name;
})
.catch(err => {
console.error('Error from fetcher', err);
});
}
};
And
import AuthService from "#/AuthService";
export default {
name: "MainNav",
data() {
return {
name: ""
};
},
async created() {
try {
this.name = await AuthService.getProfile();
} catch(error) {
// handle
}
}
};
</script>
Or without async (add one more then)
import AuthService from "#/AuthService";
export default {
name: "MainNav",
data() {
return {
name: ""
};
},
created() {
AuthService.getProfile().then((userName) => this.name = userName))
.catch((error) => { /* handle */ })
}
};
</script>
Here is the source for a self contained Facebook login component in Vue. I'm trying to use this with AWS Amplify. I successfully get the Facebook login screen to appear and I am able to get an identity id in AWS cognito. However, things aren't quite working.
<template>
<div>
<Button
class="FacebookButton"
v-text="buttonText"
#click="handleClick"
:disabled="isLoading"
/>
</div>
</template>
<script>
import Auth from "#aws-amplify/auth";
import { AmplifyEventBus } from "aws-amplify-vue";
export default {
name: "FacebookButton",
components: {
},
data() {
return {
buttonText: "Login to Facebook",
isLoading: true
};
},
computed: {},
async mounted() {
this.loadFacebookSDK();
await this.waitForInit();
this.isLoading = false;
},
beforeCreate() {},
methods: {
waitForInit() {
return new Promise(res => {
const hasFbLoaded = () => {
if (window.FB) {
res();
} else {
setTimeout(hasFbLoaded, 300);
}
};
hasFbLoaded();
});
},
statusChangeCallback(response) {
if (response.status === "connected") {
this.handleResponse(response.authResponse);
} else {
this.handleError(response);
}
},
checkLoginState() {
window.FB.getLoginStatus(this.statusChangeCallback);
},
handleClick() {
window.FB.login(this.checkLoginState, { scope: "public_profile,email" });
},
handleError(error) {
alert(error);
},
async handleResponse(data) {
const { email, accessToken: token, expiresIn } = data;
const expires_at = expiresIn * 1000 + new Date().getTime();
const user = { email };
this.isLoading = true;
try {
//const response = await Auth.federatedSignIn(
await Auth.federatedSignIn("facebook", { token, expires_at }, user);
this.isLoading = false;
AmplifyEventBus.$emit("authState", "signedIn");
//this.props.onLogin(response);
} catch (e) {
this.isLoading = false;
this.handleError(e);
}
},
loadFacebookSDK() {
window.fbAsyncInit = function() {
window.FB.init({
appId: "yourappidhere",
xfbml: true,
version: "v3.2"
});
window.FB.AppEvents.logPageView();
};
(function(d, s, id) {
var js,
fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) {
return;
}
js = d.createElement(s);
js.id = id;
js.src = "https://connect.facebook.net/en_US/sdk.js";
fjs.parentNode.insertBefore(js, fjs);
})(document, "script", "facebook-jssdk");
}
}
};
</script>
<style scoped>
</style>
This code is a port from a React example here: https://serverless-stack.com/chapters/facebook-login-with-cognito-using-aws-amplify.html
I'm trying to make this Facebook login work with the existing Amplify authenticator state management system by emitting the change to authState as follows:
AmplifyEventBus.$emit("authState", "signedIn");
This causes the Authenticator widget to 'disappear', but it doesn't let me get into my app. I'm not quite sure where to go next.