SvelteKit + Hooks and MSAL.js using Azure AD B2C results in non_browser_environment - authentication

I am new to SvelteKit and i am trying to use MSAL.js with SvelteKit, the issue is i want to implement something similar to an AuthGuard/HttpInterceptor which checks to see if the user is still logged in as they navigate around the SPA or call the external API.
I am using the OAuth 2.0 authorization code flow in Azure Active Directory B2C
within in my auth.ts file i have the following code
let accountId: string = "";
const signIn = () => {
try {
msalInstance.loginRedirect(loginRequestWithApiReadWrite);
} catch (error) {
isAuthenticated.set(false);
console.warn(error);
}
}
// This captures the response from using the redirect flow to login
await msalInstance.handleRedirectPromise()
.then(response => {
if (response) {
if (response.idTokenClaims['tfp'].toUpperCase() === b2cPolicies.names.signUpSignIn.toUpperCase()) {
handleResponse(response);
}
}
})
.catch(error => {
console.log(error);
});
async function handleResponse(response: msal.AuthenticationResult) {
if (response !== null) {
user.set(response);
isAuthenticated.set(true);
setAccount(response.account);
} else {
selectAccount();
}
}
function selectAccount() {
const currentAccounts = msalInstance.getAllAccounts();
if (currentAccounts.length < 1) {
return;
} else if (currentAccounts.length > 1) {
const accounts = currentAccounts.filter(account =>
account.homeAccountId.toUpperCase().includes(b2cPolicies.names.signUpSignIn.toUpperCase())
&&
account.idTokenClaims?.iss?.toUpperCase().includes(b2cPolicies.authorityDomain.toUpperCase())
&&
account.idTokenClaims.aud === msalConfig.auth.clientId
);
if (accounts.length > 1) {
if (accounts.every(account => account.localAccountId === accounts[0].localAccountId)) { console.log("Multiple accounts belonging to the user detected. Selecting the first one.");
setAccount(accounts[0]);
} else {
console.log("Multiple users accounts detected. Logout all to be safe.");
signOut();
};
} else if (accounts.length === 1) {
setAccount(accounts[0]);
}
} else if (currentAccounts.length === 1) {
setAccount(currentAccounts[0]);
}
}
// in case of page refresh
selectAccount();
function setAccount(account: msal.AccountInfo | null) {
if (account) {
accountId = account.homeAccountId;
}
}
const authMethods = {
signIn,
getTokenRedirect
}
In a +page.svelte file i can then import the authMethods no problem, MSAL redirects me to the microsoft sign in page, i get redirected back and can then request an access token and call external API, great all is well.
<script lang='ts'>
import authMethods from '$lib/azure/auth';
<script>
<button on:click={authMethods.signIn}>Sign In</button>
However, the issue i am having is trying to implement this so i can check to see if the user is logged in against Azure B2C using a hook.server.ts file automatically. I would like to check a variable to see if the user is authenticated and if they arnt the hooks.server will redirect them to signUp by calling the authMethod within the hook, and the user will be automatically redirected to the sign in page.
In the hooks.server.ts i have the following code:
export const handle: Handle = (async ({ event, resolve }) => {
if (isAuthenticated === false) {
authRedirect.signIn();
msalInstance.handleRedirectPromise().then((response) => {
if (response) {
console.log('login with redirect succeeded: ', response)
isAuthenticated = true;
}
}).catch((error) => {
console.log('login with redirect failed: ', error)
})
}
const response = await resolve(event);
return response;
}) satisfies Handle;
When i navigate around the SvelteKit SPA, MSAL.js keeps throwing the error below, which i know is because i am running the code from the server flow rather than in the browser, so it was my understanding that if i implement the handleRedirectPromise() in both the auth.ts file and hooks.server.ts this would await the response from the signIn event and so long as i got a response i can then set isAuthenticated to true.
errorCode: 'non_browser_environment',
errorMessage: 'Login and token requests are not supported in non-browser environments.',
subError: ''

Are you required to use the MSAL library? I have got it working with https://authjs.dev/. I was using Active Directory -https://authjs.dev/reference/oauth-providers/azure-ad but there is also a flow for B2C https://authjs.dev/reference/oauth-providers/azure-ad-b2c which I haven't tried.
Then in the hooks.server.js you can do something like the below.
import { sequence } from '#sveltejs/kit/hooks';
import { redirect } from '#sveltejs/kit';
import { SvelteKitAuth } from '#auth/sveltekit';
import AzureADProvider from '#auth/core/providers/azure-ad';
import {
AZURE_AD_CLIENT_ID,
AZURE_AD_CLIENT_SECRET,
AZURE_AD_TENANT_ID
} from '$env/static/private'
const handleAuth = SvelteKitAuth({
providers: [
AzureADProvider({
clientId: AZURE_AD_CLIENT_ID,
clientSecret: AZURE_AD_CLIENT_SECRET,
tenantId: AZURE_AD_TENANT_ID
})
]
});
async function isAuthenticatedUser({ event, resolve }) {
const session = await event.locals.getSession();
if (!session?.user && event.url.pathname !== '/') {
throw redirect(302, '/');
} else if (session?.user && event.url.pathname === '/') {
throw redirect(302, '/dashboard');
}
const response = await resolve(event);
return response;
}
export const handle = sequence(handleAuth, isAuthenticatedUser);

Related

React Native Facebook User Photos is Denied

I have a react native app and i can login with facebook. However I can't get the users photo. First of all FB hash key is correct and my app is in live mode. I sent the app to APP REVIEW and the photos are always denied by team and they are telling me they can't get the photos of the users. I use "react-native-fbsdk-next": "^4.3.0" and we use our own api url for photos, not using Graph Api of FB. There is [user_photos] as well beside public_profile. Does anyone know the reason for this ? After I login to Facebook, i try to upload photo via FB and it displays a pop up saying " facebook photos permission is denied. This permission allows your app to read photos of Facebook". Why facebook team denies user photo access ? what else should do to make it work ? My login code implementation is below. I could not find anything on Google regarding this kind of issue. Please help
export const facebookLogin = snackBarBottomMargin => {
return async dispatch => {
try {
const result = await LoginManager.logInWithPermissions([
'public_profile',
'user_photos',
]);
if (!result.isCancelled) {
const data = await AccessToken.getCurrentAccessToken();
if (data && data.accessToken) {
await storage.storeData(
PREFERENCES.FB_ACCESS_TOKEN,
JSON.stringify(data),
);
return data;
} else {
console.log('Facebook result fetch token error cancelled');
return false;
}
} else {
console.log('Login cancelled');
return false;
}
} catch (error) {
dispatch(
showSnackbar(strings.login.facebookLoginError, snackBarBottomMargin),
);
return false;
}
};
};
export function handleFetchFBPhotos(accessToken, after) {
return async dispatch => {
function onSuccess(success) {
dispatch(fetchMediaSuccess(success));
console.log('success', success);
return success;
}
function onError(error) {
dispatch(fetchMediaFailed(error));
console.log('error', error);
return error;
}
try {
dispatch(fetchMediaRequest(true));
const config = {
baseURL: Config.FACEBOOK_BASE_URL,
params: {
type: 'uploaded',
fields: 'images',
after,
},
headers: {Authorization: `Bearer ${accessToken}`},
};
const response = await axios.get(FACEBOOK_PHOTOS, config);
if (response.data && response.data.data) {
console.log('response.data', response.data);
console.log('response.data.data', response.data.data);
console.log('onSuccess(response.data)', onSuccess(response.data));
return onSuccess(response.data);
}
} catch (error) {
const errorObj = getErrorResponse(
error,
Config.FACEBOOK_BASE_URL + FACEBOOK_PHOTOS,
);
console.log('onError(errorObj.message)', onError(errorObj.message));
return onError(errorObj.message);
}
};
}

How do I write tests for pages middleware (Next 12)?

I have added some logic in my pages middleware (using Next 12) and would like to add tests now but am pretty lost on how to get that started. Can someone direct me to a tutorial or resource that shows a complete example of middleware being tested?
Specifically this is what my middleware is doing:
export function middleware(request: NextRequest) {
// Redirect a user if they don't have an auth token and are not the admin role
if (request.nextUrl.pathname.startsWith('/admin')) {
const authTokenCookie = request.cookies.token;
const parsedToken = authTokenCookie ? jose.decodeJwt(authTokenCookie) : null;
const role = typeof parsedToken === 'object' ? parsedToken?.role : null;
if (!authTokenCookie || !role || role !== USER_ROLES.admin) {
return NextResponse.redirect(new URL('/', request.url));
}
}
// Redirect the user if a query parameter is present
if (request.nextUrl.pathname === '/' && request.nextUrl.searchParams.has('directToStore')) {
NextResponse.redirect(new URL('/store', request.url));
}
return NextResponse.next();
}
This is how I ended up testing my middleware:
import { middleware } from '../pages/_middleware';
import { NextResponse, NextRequest } from 'next/server';
import * as jose from 'jose';
describe('Middleware', () => {
const redirectSpy = jest.spyOn(NextResponse, 'redirect');
afterEach(() => {
redirectSpy.mockReset();
});
it('should redirect to the homepage if visiting an admin page as a user without an auth token', async () => {
const req = new NextRequest(new Request('https://www.whatever.com/admin/check-it-out'), {});
req.headers.set(
'cookie',
'token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2NTg3NjczMjYsImV4cCI6MTY5MDMwMzMyNiwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsInJvbGUiOiJ1c2VyIn0.G7rkptAKt1icBp92KcHYpGdcWOnn4gO8vWiCMtIHc0c;',
);
const { role } = jose.decodeJwt(req.cookies.token);
await middleware(req);
expect(role).toEqual('user');
expect(redirectSpy).toHaveBeenCalledTimes(1);
expect(redirectSpy).toHaveBeenCalledWith(new URL('/', req.url));
});
it('should redirect to the store if the directToStore query param is set', async () => {
const req = new NextRequest(new Request('https://www.whatever.com'), {});
req.nextUrl.searchParams.set('directToStore', 'true');
await middleware(req);
expect(redirectSpy).toHaveBeenCalledTimes(1);
expect(redirectSpy).toHaveBeenCalledWith(new URL('/store', req.url));
});
});

Nuxt - is it possible to check if a user is logged in from SSR?

I created a Nuxt app that uses Django on the backend, i'm using the standard Django Session Authentication, so when i log in from Nuxt, a session cookie is set in my browser.
I've been trying for days to find a way to restrict some pages to authenticated users only, but i don't seem to find any working approach to do that. I need to check if the user is logged in before the page is loaded, so i tried to use a middleware but middleware won't work at all because the middleware is executed from server side (not client side) so there won't be any cookie in the request.
At this point, is there any other way to do this from SSR? Here is my request:
export default async function (context) {
axios.defaults.withCredentials = true;
return axios({
method: 'get',
url: 'http://127.0.0.1:8000/checkAuth',
withCredentials: true,
}).then(function (response) {
//Check if user is authenticated - response is always False
}).catch(function (error) {
//Handle error
});
}
If you are running Nuxt in SSR mode as server, you can access the cookie headers to find out if the user has a certain cookie. Packages like cookieparser (NPM) can easily do that for you.
But as you already found out, you can't do that in a middleware. What you could use instead is the nuxtServerInit action in your store (Docs). This action is run on the server and before any middleware gets executed. In there you can use cookieparser to get the user's cookies, authenticate them and save the any information you need in the store.
Later you can access the store in your middleware and for example redirect the user.
actually you can get cookies in a middleware.... Ill put my example, but the answer above is more correct .
middleware/auth.js
import * as cookiesUtils from '~/utils/cookies'
export default function ({ route, req, redirect }) {
const isClient = process.client
const isServer = process.server
const getItem = (item) => {
// On server
if (isServer) {
const cookies = cookiesUtils.getcookiesInServer(req)
return cookies[item] || false
}
// On client
if (isClient) {
return cookiesUtils.getcookiesInClient(item)
}
}
const token = getItem('token')
const { timeAuthorized } = cookiesUtils.authorizeProps(token)
const setRedirect = (routeName, query) => {
return redirect({
name: routeName,
query: query
? {
redirect: route.fullPath
}
: null
})
}
// strange bug.. nuxt cant redirect '/' to '/login'
if (route.path === '/') {
setRedirect('users')
}
if (!route.path.match(/\/login\/*/g) && !timeAuthorized) {
setRedirect('login', true)
}
}
utils/cookies.js
import Cookie from 'js-cookie'
import jwtDecoded from 'jwt-decode'
/*
TOKEN
*/
// Get server cookie
export const getcookiesInServer = (req) => {
const serviceCookie = {}
if (req && req.headers.cookie) {
req.headers.cookie.split(';').forEach((val) => {
const parts = val.split('=')
serviceCookie[parts[0].trim()] = (parts[1] || '').trim()
})
}
return serviceCookie
}
// Get the client cookie
export const getcookiesInClient = (key) => {
return Cookie.get(key) || false
}
export const setcookiesToken = (token) => {
Cookie.set('token', token)
}
export const removecookiesToken = () => {
Cookie.remove('token')
}
export const authorizeProps = (token) => {
const decodeToken = token && jwtDecoded(token)
const timeAuthorized = (decodeToken.exp > Date.now() / 1000) || false
return {
timeAuthorized
}
}

Migrate ADAL.js to MSAL.js

I have a SPA which uses the solution provided here to authenticate with Azure AD and everything works as expected. Now I want to migrate this to use MSAL.js.
I use below for login:
import * as MSAL from 'msal'
...
const config = {
auth: {
tenantId: '<mytenant>.com',
clientId: '<myclientid>',
redirectUri: <redirecturi>,
},
cache: {
cacheLocation: 'localStorage',
}
};
const tokenRequest = {
scopes: ["User.Read"]
};
export default {
userAgentApplication: null,
/**
* #return {Promise}
*/
initialize() {
let redirectUri = config.auth.redirectUri;
// create UserAgentApplication instance
this.userAgentApplication = new MSAL.UserAgentApplication(
config.auth.clientId,
'',
() => {
// callback for login redirect
},
{
redirectUri
}
);
// return promise
return new Promise((resolve, reject) => {
if (this.userAgentApplication.isCallback(window.location.hash) || window.self !== window.top) {
// redirect to the location specified in the url params.
}
else {
// try pull the user out of local storage
let user = this.userAgentApplication.getUser();
if (user) {
resolve();
}
else {
// no user at all - go sign in.
this.signIn();
}
}
});
},
signIn() {
this.userAgentApplication.loginRedirect(tokenRequest.scopes);
},
And then I use below to get the token:
getCachedToken() {
var token = this.userAgentApplication.acquireTokenSilent(tokenRequest.scopes);
return token;
}
isAuthenticated() {
// getCachedToken will only return a valid, non-expired token.
var user = this.userAgentApplication.getUser();
if (user) {
// get token
this.getCachedToken()
.then(token => {
axios.defaults.headers.common["Authorization"] = "Bearer " + token;
// get current user email
axios
.get('<azureapi-endpoint>' + '/GetCurrentUserEmail')
.then(response => { })
.catch(err => { })
.finally(() => {
});
})
.catch(err => { })
.finally(() => { });
return true;
}
else {
return false;
}
},
}
but after login I get below error:
Access to XMLHttpRequest at 'https://login.windows.net/common/oauth2/authorize?response_type=code+id_token&redirect_uri=<encoded-stuff>' (redirected from '<my-azure-api-endpoint>') from origin 'http://localhost:8080' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Also the token that I get seems to be invalid as I get 401 errors trying to call api using the token. Upon checking the token against https://jwt.io/ I get an invalid signature.
I really appreciate anyone's input as I've already spent good few days and haven't got anywhere yet.
I'm not sure if this is your issue. however, for msal.js, in the config, there is no tenantId parameter, it's supposed to be authority. Here is a sample for graph api using msal.js
https://github.com/Azure-Samples/active-directory-javascript-graphapi-v2
specifically: the config is here: https://github.com/Azure-Samples/active-directory-javascript-graphapi-v2/blob/quickstart/JavaScriptSPA/authConfig.js
as per here, https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-js-initializing-client-applications it is supposed to be hitting login.microsoftonline.com not login.windows.net

Implementing authentication flow with react-navigation and redux

I do't know what is happening here. I made simple app with 2 screens, on login success i want to change screen on HomePage. I am using redux, redux-saga and axios with react-native
My submit function on login screen is:
async submit() {
if(this.state.email.length == 0 || this.state.password.length == 0) {
alert("Filed is required")
} else {
if(typeof this.state.email !== 'undefined') {
if(!this.state.email.match(/[^#]+#[^.]+\..+/)) {
alert("E-mail format wrong!")
} else {
console.log('ok auth');
let user = {
email: this.state.email,
password: this.state.password
}
this.props.login(user)
}
}
}
}
Login saga write token in AsyncStorage, on login success and it works fine
export function* loginUser(action) {
const response = yield call(login_api, action.payload)
if(!response || !response.data) {
console.log('wrong login 1: ', response);
//status 422 Unprocessable Entity
return yield put(login_failure('Internal server error for login user'))
}
if(response.status === 200) {
deviceStorage.saveItem('token', response.data.token)
return yield put(login_success(response.data, response.data.token))
} else {
console.log('wrong login 2');
return yield put(login_failure('Error for login user'))
}
}
On login success, reducer returns token i Login.js and then I want to redirect in HomePage. I want to logout user every time when he comes on login page, because I use componentDidMount()
async componentDidMount() {
await deviceStorage.deleteToken('token')
}
componentWillReceiveProps(nextProps) {
console.log('enxtProps: ', nextProps.token.length);
if(nextProps.token.length > 0) {
//TOKEN EXISTS!!! length = 365
this.props.navigation.navigate('HomePage')
} else {
console.log('Error login');
}
this.setState({
token: nextProps.token
})
}
this.props.navigation.navigate('HomePage') in componentWillReceiveProps doesn't redirect on HomePage.
Can someone tell me why?
I have a couple of suggestions for your code.
In Login-Saga wait until token saved
deviceStorage.saveItem('token', response.data.token)
change to
yield deviceStorage.saveItem('token', response.data.token)
Don't use componentWillReceiveProps. It is marked as unsafe in react & remove after version 17. Use componentDidUpdate as below.
componentDidUpdate(prevProps) {
// Typical usage (don't forget to compare props)
if (this.props.token !== prevProps.token && this.props.token.length > 0) {
this.props.navigation.navigate('HomePage')
} else {
console.log('Error login');
}
}