I have created two React components, Login, and Secure. Login hosts the FacebookLogin component found in this package: https://www.npmjs.com/package/#greatsumini/react-facebook-login, and I get a successful response from Facebook by the login attempt when the button is pressed. This is supposed to navigate to the Secure page when successful.
The problem is that I can just navigate directly to the Secure component in the URL, (I'm using react-router-dom package for this), but I want any attempt to navigate to a secure page to be redirected to the Login page.
How can I do this?
According to that component's documentation, you can get the login status in code:
FacebookLoginClient.login((res) => {
// "res" contains information about the current user's logged-in status
});
So in any component that needs to know the status, simply reference the library:
import { FacebookLoginClient } from '#greatsumini/react-facebook-login';
And use that FacebookLoginClient.login to check the user's logged-in status. If the status doesn't meet the criteria you define, redirect the user. (For example, you can check this in a useEffect when the component first loads, or perhaps on every render if you're paranoid about it.)
For more detail on rendering the target component, since useEffect occurs after the initial render, you can rely on additional state to check. For example, consider this structure:
const [canRender, setCanRender] = useState(false);
useEffect(() => {
FacebookLoginClient.login((res) => {
// check the status and call "setCanRender" accordingly
});
}, []);
if (!canRender) return <>Checking authentication...</>;
// your existing return for the component goes here
The idea here is to default the component to a kind of "loading state" and only render the "secure" content after the authentication has been verified.
Related
I want to be able to redirect to another page in Next.js passing data. The reason about why I want to do it is the following:
I am working on a project in which the user can be an entity or not. If it is not an entity the page about the user will be / and if it is an entity the page about the user will be /entity.
When I go to the page / I use getServerSideProps to fetch all the data about the user and send it as a prop. However, if the fetched user is an entity I redirect to /entity. Then, I use getServerSideProps in /entity to fetch all the data about the user (that is an entity).
I am doing two requests when I only should do one of them. If I manage to redirect to /entity passing the data that I already fetched I wouldn't have this problem. Is there a way I can do it?
// index.js
export async function getServerSideProps(context) {
const user = await getUser(accessCookies(context));
if (user.isEntity)
return { redirect: { destination: "/entity", permanent: false } }; // Would like to send user
return { props: { user} };
}
I don't see any way to achieve it and I don't even know if it is possible.
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.
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.
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.
Context: I have a <Header /> component that has a button which redirects the user to a <SignIn /> component. The <Header /> component is always displayed, however when the user is signed in, the Login button disappears and replaced by the user's name.
I will not go in detail about the authentication mechanism, but let's just say it is based on a JWT that is stored in localStorage. I have the following piece of code in <Header /> in order to read the cookie and determine whether to show the login button or not:
componentDidMount() {
const token = localStorage.getItem("token");
if (token) {
this.props.fetchUser();
}
}
...
However this only works on a browser refresh or if I use the native JS window.reload(). If I use react router redirections, the Login button in the <Header /> component is not refreshed and still displays (although the cookie is set).
I am wondering what is the best practice, and avoiding the whole page refresh...
So it is super pseudo code, but hopefully that would be enough :)
class App {
const handleLoggedIn = () => {
const token = readFromLocalStorage()
if (token) {
this.setState({token})
}
}
<Header token={this.state.token} />
<Login onLogin={this.handleLoggedIn}
}
class Login {
// After loging in and setting your token to your local storage:
this.props.onLogin()
}
class Header {
if (this.props.token)
<User />
else
<Login />
}
So once you are logged in and set your token to the local storage, you just call the parent function handleLoggedIn which will try to retrieve the token from your local storage. If it is set, it will re-set it as your App state, which will refresh your Header component as one of its props has been updated
(But that mainly depends on how you've implemented the rest of your code and how easily you can access the parent function from your login flow as pointed by Dubes)
Two possible scenarios I can think of:
Scenario 1
You've someway of hooking into the flow when user signs in. For e.x. maybe your auth system is setting a success flag in header/query or perhaps the component which writes the jwt to the localstorage is in your control/tree.
In that case, you should be derive that information and be able to set a prop which your header component can use.
Scenario 2
You don't have any way of hooking into your auth workflow and can only read the localstorage item. For e.x. a user has signed on with SSO and you want to reflect that without having to refresh the entire page.
I think you can use a setInterval to read the state of the cookie and set a state in your header component. As long as you are not doing intensive calculations in your setInterval code, it should not be an overhead. Do remember to clearInterval on unmount.
Take a look at this example in official docs for inspriration.