Issues using Azure Active Directory (ADAL) with Aurelia - azure-active-directory

I've added Azure Active Directory Library (ADAL) to an Aurelia CLI 0.31.3 project and I appear to be out of my element.
Here is my repository.
The issues that I'm running into are:
Accessing the "Reports" page for the first time allows you to log in.
When it returns from the Azure AD sign-in, the Aurelia app reloads
twice.
Clicking "Log In" when on the "Home" page allows you to log
in. When it returns from the Azure AD sign-in, the URL contains
"/token_id=XXXX" and the Aurelia router errors stating "Route not
found".
Some notes on the project:
There are only two views. "Home" does not require authentication. "Reports" does.
./src/app.ts , ./src/authorizeStep.ts , and ./src/sessionState.ts , should be the only places any authentication code exists.
Any insight as to how to resolve these would be greatly appreciated!

#juunas probably has a working solution, but my take on this would have been slightly different.
The error pretty much says it: "Route not found". Aurelia is trying to match "token_id" to a route while that value should probably be ignored after your AuthorizeStep used it.
It may suffice to simply add it to your home routes like so:
{ route: ['', 'home', 'token_id'], name: 'home', moduleId: 'resources/views/home/home', nav: true, title: "Home" },
If it still doesn't match, you could add a wildcard as well: token_id*
That will solve the router error. You haven't mentioned whether the authentication itself works or not - if the router error was the only problem, this should do the trick.
EDIT
To follow up on my comment, as an alternative to using a separate view/viewmodel as a route you can also do this:
config.map({
name: 'logInRedirectCallback',
navigationStrategy: (instruction: NavigationInstruction) => {
const token = instruction.getWildcardPath();
// [...] do magic / pass the token to wherever it's needed
instruction.router.navigateToRoute(""); // or any page that makes sense after logging in
},
route: 'token_id=*',
});
I have not tested this so you may need to tweak the exact place of that wildcard. This works under the assumption that token_id is matched as part of the path, not part of the query. But the general idea is to just intercept that token, process it however you need to and then go to some existing page.

I don't have much experience with Aurelia, but here are the steps I took to make it work at least partially.
Hopefully you can get it working fully :)
First, I enabled verbose logging for ADAL.JS in sessionState.ts:
constructor(aureliaConfiguration, sessionState) {
this.aureliaConfiguration = aureliaConfiguration;
if (!this.authContext) {
this.authContext = new AuthenticationContext(
{
cacheLocation: "localStorage",
clientId: this.aureliaConfiguration.get('azureADApplicationId'),
tenant: this.aureliaConfiguration.get('azureADTenant'),
postLogoutRedirectUri: window.location.origin
}
);
Logging = {
level: 3,
log: (msg) => console.log(msg)
}
}
this.appName = this.aureliaConfiguration.get('name');
this.appApiKey = this.aureliaConfiguration.get('api.key');
}
Then I found ADAL giving an error about invalid state.
I noticed the URL was not really what it should be.
Usually the Id token is in a fragment, e.g. #id_token=asdasdasdas....
Of course this is just the framework's feature of showing pretty client-side routes.
Turns out there is a way to tell Aurelia not to modify URLs with hashes, while still using the normal routing links:
configureRouter(config: RouterConfiguration, router: Router): void {
this.sessionState.router = router;
config.title = 'Aurelia';
config.options.root = '/';
config.options.pushState = true;
config.options.hashChange = false;
config.map([
{ route: ['', 'home'], name: 'home', moduleId: 'resources/views/home/home', nav: true, title: "Home" },
{ route: ['reports'], name: 'reports', moduleId: 'resources/views/reports/reports', nav: true, title: "Reports", settings: { auth: true } }
]);
config.addAuthorizeStep(AuthorizeStep);
}
I added one line there: config.options.hashChange = false;.
After doing that, ADAL logged a successful authentication.
But I did say the solution was partial.
The login button still says "Log in".
After a page refresh it does change to "Log out", but that is one thing that needs to be fixed.
Also, clicking on "reports" does cause a redirect to AAD, but it specifies the redirect URL as http://localhost:9000/reports.
The problem is that we would need to configure every single client-side route as a reply URL for this to work.
So instead of using the full URL, you will need to use just the domain in authorizeStep.ts:
if (!user) {
this.sessionState.authContext.config.redirectUri = window.location.origin;
this.sessionState.authContext.login();
}

Related

Prevent flash of wrong page in NextJS app after MSAL-React redirect to/from Azure AD B2C

Context & Reproducible Scenario
I'm using the combination of these libraries and tools:
NextJS 12+ (based on React 18+)
MSAL-Browser 2.25+ and MSAL-React 1.6+ (Microsoft's libs for OpenID login against Azure B2C)
I'm using the Auth Code + PKCE redirect flow so this is the flow for users:
They land on /, the home page
They click a /me router link
They go to Azure B2C to log in because said page has this logic:
<MsalAuthenticationTemplate
interactionType={InteractionType.Redirect}
authenticationRequest={loginRequest}>
where loginRequest.state is set to router.asPath (the "intended" page: /me)
Note that the page is also wrapped in a <NoSsr> component based off Stack Overflow.
User logs in on Azure B2C, gets redirected back to my app at / (the root)
â›” Problem: the user now briefly sees the / (home) page
After a very brief moment, the user gets sent to /me where they are signed in
The MSAL docs don't seem to have much on the state property from OIDC or this redirect behavior, and I can't find much about this in the MSAL sample for NextJS either.
In short: the issue
How do I make sure MSAL-React in my NextJS application send users to the "intended" page immediately on startup, without briefly showing the root page where the Identity Server redirects to?
Relevant extra information
Here's my custom _app.js component, which seems relevant because it is a component that triggers handleRedirectPromise which causes the redirect to intended page:
export default function MyApp({ Component, pageProps }) {
return (
<MsalProvider instance={msalInstance}>
<PageHeader></PageHeader>
<Component {...pageProps} />
</MsalProvider>
);
}
PS. To help folks searching online find this question: the behavior is triggered by navigateToLoginRequestUrl: true (is the default) in the configuration. Setting it to false plainly disables sending the user to the intended page at all.
Attempted solutions with middleware
I figured based on how APP_INITIALIZERs work in Angular, to use middleware like this at some point:
// From another file:
// export const msalInstance = new PublicClientApplication(msalConfig);
export async function middleware(_request) {
const targetUrlAfterLoginRedirect = await msalInstance.handleRedirectPromise()
.then((result) => {
if (!!result && !!result.state) {
return result.state;
}
return null;
});
console.log('Found intended target before login flow: ', targetUrlAfterLoginRedirect);
// TODO: Send user to the intended page with router.
}
However, this logs on the server's console:
Found intended target before login flow: null
So it seems middleware is too early for msal-react to cope with? Shame, because middleware would've been perfect, to allow as much SSR for target pages as possible.
It's not an option to change the redirect URL on B2C's side, because I'll be constantly adding new routes to my app that need this behavior.
Note that I also tried to use middleware to just sniff out the state myself, but since the middleware runs on Node it won't have access to the hash fragment.
Animated GIF showing the flashing home page
Here's an animated gif that shows the /home page is briefly (200ms or so) shown before /me is properly opened. Warning, gif is a wee bit flashy so in a spoiler tag:
Attempted solution with custom NavigationClient
I've tried adding a custom NavigationClient to more closely mimic the nextjs sample from Microsoft's repository, like this:
import { NavigationClient } from "#azure/msal-browser";
// See: https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-react/docs/performance.md#how-to-configure-azuremsal-react-to-use-your-routers-navigate-function-for-client-side-navigation
export class CustomNavigationClient extends NavigationClient {
constructor(router) {
super();
this.router = router;
}
async navigateInternal(url, options) {
console.log('đź‘Ť Navigating Internal to', url);
const relativePath = url.replace(window.location.origin, "");
if (options.noHistory) {
this.router.replace(relativePath);
} else {
this.router.push(relativePath);
}
return false;
}
}
This did not solve the issue. The console.log is there allowing me to confirm this code is not run on the server, as the Node logs don't show it.
Attempted solution: go through MSAL's SSR docs
Another thing I've tried is going through the documentation claiming #azure/msal-react supports Server Side Rendering (SSR) but those docs nor the linked samples demonstrate how to solve my issue.
Attempted solution in _app.tsx
Another workaround I considered was to sniff out the hash fragment client side when the user returns to my app (and make sure the intended page is also in that state). I can successfully send the OpenID state to B2C like this...
const extendedAuthenticationRequest = {
...authenticationRequest,
state: `~path~${asPath}~path~`,
};
...and see it returned in the Network tab of the dev tools.
However, when I try to extract it in my _app.tsx still doesn't work. I tried this code from another Stack Overflow answer to get the .hash:
const [isMounted, setMounted] = useState(false);
useEffect(() => {
if (isMounted) {
console.log('====> saw the following hash', window.location.hash);
const matches = /~path~(.+)~path~/.exec(window.location.hash);
if (matches && matches.length > 0 && matches[1]) {
const targetUrlAfterOpenIdRedirect = decodeURIComponent(matches[1]);
console.log("Routing to", targetUrlAfterOpenIdRedirect);
router.replace(targetUrlAfterOpenIdRedirect);
}
} else {
setMounted(true);
}
}, [isMounted]);
if (!isMounted) return null;
// else: render <MsalProvider> and the intended page component
This does find the intended page from the state and executes routing, but still flashes the /home page before going to the intended page.
Footnote: related GitHub issue
Submitted an issue at MSAL's GitHub repository too.

How to have dynamic redirect URLs in Next.js?

I'm working on a Next.js/Django project, which the user is able to add some redirect logic from the admin panel like:
[
{ source: "/about", destination: "google.com" },
{ source: "/about1", destination: "google1.com" },
{ source: "/about2", destination: "google2.com" },
]
and the web application should be able to handle these dynamic redirects.
As the Nextjs docs says, we can do this in next.config.js. The problem is that we can't have dynamic data in next.config.js. With every change in this file, server must be restarted.
Here we need a logic that gets the urls using an API on website load, loops through them and listens for every route calls to see if they match the redirect data or not.
I have tried some other ways too, like trying to use useEffect, but this way causes the website to render 404 page first and then it redirects to the desired url, which is not that nice for user experience viewpoints.
You can use Next.js Middleware to fetch the dynamic redirects array from the API, and add your own logic to handle the redirection.
Unlike redirects in the next.config.js that run at build time, Next.js Middleware runs on each incoming request to your pages and is able to dynamically fetch the redirects every time.
export async function middleware(req) {
// Fetch redirects array from API
const res = await fetch('https://example.com/api/redirects');
const redirects = await res.json();
/* Assuming the returned `redirects` array would have the following structure:
[
{ source: '/about-us', destination: '/about' },
{ source: '/google', destination: 'https://google.com' }
]
*/
// Find the matching redirect object
const redirect = redirects.find((redirect) => req.nextUrl.pathname === redirect.source);
if (redirect) {
if (redirect.destination.startsWith('http')) {
// Handle external URL redirects
return NextResponse.redirect(new URL(redirect.destination));
}
// Handle internal URL redirects
return NextResponse.redirect(new URL(redirect.destination, req.url));
}
// Continue if no matching redirect is found
return NextResponse.next();
}

React-Native: Deep Linking with required fetch Call in HomeScreen

I'm new on react-native and deep linking. I have a react-native App with BottomBar and StackNavigator.
First Tab "Stöbern" with First StackScreen HomeScreen has a fetch Call in ComponentDidMount for renew the session Token und set a variable for "isLoggedIn". For now, i don't have Deep Linking. For now the Startscreen is always HomeScreen with this fetch call to renew the token and check if token is valid, then set it to "isLoggedIn".
HomeScreen is a public screen, Favorite is a member screen.
Now i try deep linking.
My linking.js:
const config = {
screens: {
Home: {
path: 'home',
screens:{
Stöbern: {
path: 'stöbern',
screens: {
HomeScreen: {
path: 'home',
}
}
},
Favoriten: {
path: 'favorite',
screens: {
FavoriteScreen: {
path: 'favorite',
}
}
}
}
In StackNavigator I have a check module:
<HomeStack.Screen name="FavoriteScreen" component={RequireAuthentication(FavoriteScreen, global.isLoggedIn)} options={{ headerShown: false }} />
If my App is open and try:
npx uri-scheme open demo://app/home/favorite/favorite --android
it works fine, because the variable IsLoggedIn is set and im routing to favoriteScreen.
If my App is closed/killed and try:
npx uri-scheme open demo://app/home/favorite/favorite --android
the logic dont go thru HomeScreen with fetch Call to set IsLoggedIn and deep linking goes to the Login Screen. This is wrong, because im logged in.
If I move the fetch call to check the token and set the variable in App.js it still doesn't work. Fetch call is calling, but the response is to late and I'm routing to login Screen.
My Question:
what is the best way for deep linking and a fetch call to check token and set a variable for "isLoggedIn"?
Another call for renew token in FavoriteScreen? But then it calls also for non deep linking calls.
What I want:
User clicks on a deep link for favoriteScreen -> open the App -> do a fetch call for renew token and set global.isLoggedIn to True -> go to favoriteScreen
I'm also trying to go always over the HomeScreen. But this doesn't work if the App is open, because the ComponentDidMount method is not calling in this case.
There're 2 scenarios:
Best-case: the app is launched and ready and the user is authenticated and s/he can safely deep-link to any protected screen.
Worst-case: app recently killed or closed, it's required to re-authenticate and validate user token which is an asynchronous operation and takes a while which makes a deep-linked fallback to the login screen.
1. Identify navigation triggered by deep linking
// First, you may want to do the default deep link handling
// Check if app was opened from a deep link
const url = await Linking.getInitialURL();
if (url != null) {
return url;
}
console.log(url)
// url contain deep linking URL
While navigation is triggered by deep linking, you need to save url in a global store like Context API or Redux. url will be needed later after getting a new token.
2. Determine whether the user needs to re-authenticate
For the worst-case scenario, you will need to authenticate the user by silently validating the old token in the background or force to authenticate manually with a login form.
3. Navigate to a deep-linked screen
At this user has been authenticated or token validated, we need to deep-link to the user destination screen.
Early, we saved deep link url in global store, we need to link to the corresponding screen. Unfortunately, url has a web-based routing structure which is not how React navigation routing works.
We need to convert web routing to React navigation routing. Below is a minified routes mapping:
const routes map = {
demo://app/home/favorite/favorite:"favorite",
demo://app/home/stöbern/home:"home",
}
With this routes map, you can use navigation.navigate(map[url]) and simply navigate to that deep-linked.
This's my opinionated brute-force solution, fellow developers should come with a lean and better solution.

How can I run a function in a service, to get an API key before anything else?

I'm running ionic-angular framework working on an app that was started before me. I need to run a function in a service to get an API key from an external server before anything else. Because I want to check if a user has an API key and check if their stored GUID is valid by making another request to the server which I need the API key for. Because I'm going to check if they need to be routed to the login page or not. When I have a route guard checking if a user is logged in my API requests aren't completed or error out because I don't have an API key yet.
If I understood your problem you're asking this: you need to check "something" to decide if the user goes to the login page or goes to another screen, at the beginning of your app, when you open it. And that "something" consists in making http requests.
I will tell you how to do it, but in my experience if you need to make HTTP requests to decide to what screen redirect the user, you will get a blank page in the meanwhile. Depending on the connection 0.1s, 0.3s, 0.7s... But it's uggly. So the alternative would be to create a "Splash" screen with a loader circle, and use that page as the initial page. Then the page checks whatever and takes you to the next page.
Now, answering your question: you need a "CanActivate". CanActivate is guard (a code) that is executed before accessing a route, to check if you can access that page or redirect the user to another page. This is very useful for local checks (like checking the local storage) but as I said, http requests will cause a blank page for a small time. Follow these steps:
1 - First you need to create the CanActivate class. That's like creating a normal Service in ionic. Then make your service implement the CanActivate interface:
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '#angular/router';
import { Observable } form 'rxjs'; // Install 'npm i rxjs' if you don't have it!
#Injectable({
providedIn: 'root'
})
export class LoginGuard implements CanActivate { }
Then this service needs to implement a function called canActivate:
export class LoginGuard implements CanActivate {
constructor(private router: Router) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) : boolean|Observable<boolean> {
return new Observable<boolean>( observer => {
// HERE CHECK
if (!condition_to_avoid_login) {
// Complete the expected route, and enter the login
observer.next(true);
observer.complete();
}
else {
// Avoid login, go somewhere else:
observer.next(false);
this.router.navigate("/my-other-page");
observer.complete();
}
})
}
}
2 - You need to add this Guard to your route. In your routing file: app-routing.module.ts, add this guard to your page:
import { LoginGuard } from '...../login-guard.service';
const routes: Routes = [
...
{
path: 'login',
loadChildren: () => import('...../login.module').then( m => m.LoginPageModule),
canActivate: [LoginGuard]
}
...
]
Now everytime the user accesses this route (/login) the LoginGuard will trigger. There you decide if continue to the login page or redirect.

Authentication with oidc-client.js and Identityserver4 in a React frontend

Lately I'm trying to set-up authentication using IdentityServer4 with a React client. I followed the Adding a JavaScript client tutorial (partly) of the IdentityServer documentation: https://media.readthedocs.org/pdf/identityserver4/release/identityserver4.pdf also using the Quickstart7_JavaScriptClient file.
The downside is that I'm using React as my front-end and my knowledge of React is not good enough to implement the same functionality used in the tutorial using React.
Nevertheless, I start reading up and tried to get started with it anyway. My IdentityServer project and API are set-up and seem to be working correctly (also tested with other clients).
I started by adding the oidc-client.js to my Visual Code project. Next I created a page which get's rendered at the start (named it Authentication.js) and this is the place where the Login, Call API and Logout buttons are included. This page (Authentication.js) looks as follows:
import React, { Component } from 'react';
import {login, logout, api, log} from '../../testoidc'
import {Route, Link} from 'react-router';
export default class Authentication extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<div>
<button id="login" onClick={() => {login()}}>Login</button>
<button id="api" onClick={() => {api()}}>Call API</button>
<button id="logout" onClick={() => {logout()}}>Logout</button>
<pre id="results"></pre>
</div>
<div>
<Route exact path="/callback" render={() => {window.location.href="callback.html"}} />
{/* {<Route path='/callback' component={callback}>callback</Route>} */}
</div>
</div>
);
}
}
In the testoidc.js file (which get's imported above) I added all the oidc functions which are used (app.js in the example projects). The route part should make the callback.html available, I have left that file as is (which is probably wrong).
The testoidc.js file contains the functions as follow:
import Oidc from 'oidc-client'
export function log() {
document.getElementById('results').innerText = '';
Array.prototype.forEach.call(arguments, function (msg) {
if (msg instanceof Error) {
msg = "Error: " + msg.message;
}
else if (typeof msg !== 'string') {
msg = JSON.stringify(msg, null, 2);
}
document.getElementById('results').innerHTML += msg + '\r\n';
});
}
var config = {
authority: "http://localhost:5000",
client_id: "js",
redirect_uri: "http://localhost:3000/callback.html",
response_type: "id_token token",
scope:"openid profile api1",
post_logout_redirect_uri : "http://localhost:3000/index.html",
};
var mgr = new Oidc.UserManager(config);
mgr.getUser().then(function (user) {
if (user) {
log("User logged in", user.profile);
}
else {
log("User not logged in");
}
});
export function login() {
mgr.signinRedirect();
}
export function api() {
mgr.getUser().then(function (user) {
var url = "http://localhost:5001/identity";
var xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.onload = function () {
log(xhr.status, JSON.parse(xhr.responseText));
}
xhr.setRequestHeader("Authorization", "Bearer " + user.access_token);
xhr.send();
});
}
export function logout() {
mgr.signoutRedirect();
}
There are multiple things going wrong. When I click the login button, I get redirected to the login page of the identityServer (which is good). When I log in with valid credentials I'm getting redirected to my React app: http://localhost:3000/callback.html#id_token=Token
This client in the Identity project is defined as follows:
new Client
{
ClientId = "js",
ClientName = "JavaScript Client",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
// where to redirect to after login
RedirectUris = { "http://localhost:3000/callback.html" },
// where to redirect to after logout
PostLogoutRedirectUris = { "http://localhost:3000/index.html" },
AllowedCorsOrigins = { "http://localhost:3000" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
}
}
Though, it seems the callback function is never called, it just stays on the callback url with a very long token behind it..
Also the getUser function keeps displaying 'User not logged in' after logging in and the Call API button keeps saying that there is no token. So obviously things are not working correctly. I just don't know on which points it goes wrong.
When inspecting I can see there is a token generated in the local storage:
Also when I click the logout button, I get redirected to the logout page of the Identity Host, but when I click logout there I don't get redirected to my client.
My questions are:
Am I on the right track implementing the oidc-client in combination with IdentityServer4?
Am I using the correct libraries or does react require different libraries than the oidc-client.js one.
Is there any tutorial where a react front-end is used in combination with IdentityServer4 and the oidc-client (without redux), I couldn't find any.
How / where to add the callback.html, should it be rewritten?
Could someone point me in the right direction, there are most likely more things going wrong here but at the moment I am just stuck in where to even begin.
IdentityServer4 is just a backend implementation of OIDC; so, all you need to do is implement the flow in the client using the given APIs. I don't know what oidc-client.js file is but it is most likely doing the same thing that you could have implemented yourself. The flow itself is very simple:
React app prepares the request and redirects the user to the Auth server with client_id and redirect_uri (and state, nonce)
IdentityServer checks if the client_id and redirect_uri match.
If the user is not logged in, show a login box
If a consent form is necessary (similar to when you login via Facebook/Google in some apps), show the necessary interactions
If user is authenticated and authorized, redirect the page to the redirect_uri with new parameters. In your case, you the URL will look like this: https://example.com/cb#access_token=...&id_token=...&stuff-like-nonce-and-state
Now, the React app needs to parse the URL, access the values, and store the token somewhere to be used in future requests:
Easiest way to achieve the logic is to first set a route in the router that resolves into a component that will do the logic. This component can be "invisible." It doesn't even need to render anything. You can set the route like this:
<Route path="/cb" component={AuthorizeCallback} />
Then, implement OIDC client logic in AuthorizeCallback component. In the component, you just need to parse the URL. You can use location.hash to access #access_token=...&id_token=...&stuff-like-nonce-and-state part of the URL. You can use URLSearchParams or a 3rd party library like qs. Then, just store the value in somewhere (sessionStorage, localStorage, and if possible, cookies). Anything else you do is just implementation details. For example, in one of my apps, in order to remember the active page that user was on in the app, I store the value in sessionStorage and then use the value from that storage in AuthorizeCallback to redirect the user to the proper page. So, Auth server redirects to "/cb" that resolves to AuthorizeCallback and this component redirects to the desired location (or "/" if no location was set) based on where the user is.
Also, remember that if the Authorization server's session cookie is not expired, you will not need to relogin if the token is expired or deleted. This is useful if the token is expired but it can be problematic when you log out. That's why when you log out, you need to send a request to Authorization server to delete / expire the token immediately before deleting the token from your storage.

Resources