keycloak Single-sign-on not working with react application - reactjs

If the keycloak user used by my react application is already signed in via another application then it shouldn't ask for authentication again via my react application (since SSO).how can i achieve this ??please provide solution to it.
i am using keycloak to authenticate a react application.keycloak authentication is working fine.tried with single-sign-on but its not working.i want that react application to work with keycloak single-sign-on i.e when keycloak user is loginned it should not ask again for login credentials.react application should work with single-sign-on.how can i achieve this ??please provide solution to it.
below is my keycloak.json
{
"realm": "Google-Auth",
"auth-server-url": "http://localhost:8180/auth",
"ssl-required": "external",
"resource": "googledemo",
"public-client": true,
"confidential-port": 0
}
below is secured.js
`import React, { Component } from 'react';
import UserInfo from './UserInfo';
import Logout from './Logout';
import Keycloak from 'keycloak-js';
class Secured extends Component {
constructor(props) {
super(props);
this.state = { keycloak: null, authenticated: false };
}
componentDidMount() {
const keycloak = Keycloak('/keycloak.json');
keycloak.init({onLoad: 'login-required'}).then(authenticated => {
this.setState({ keycloak: keycloak, authenticated: authenticated })
})
}
render() {
if(this.state.keycloak) {
if(this.state.authenticated) return (
<div>
<p>
This is a Keycloak-secured component of your application. You shouldn't be able to
unless you've authenticated with Keycloak.
</p>
<UserInfo keycloak={this.state.keycloak} />
<Logout keycloak={this.state.keycloak} />
</div>
); else return (<div>Unable to authenticate!</div>)
}
return (
<div>Initializing Keycloak...</div>
);
}
}
export default Secured;

I have come across a similar scenario. I used the same keycloak-js package as well. Instead of using that in component, you can try to create a service from it
import Keycloak from "keycloak-js";
const _kc = new Keycloak('/keycloak.json');
/**
* Initializes Keycloak instance and calls the provided callback function if successfully authenticated.
*
* #param onAuthenticatedCallback
*/
const initKeycloak = (onAuthenticatedCallback) => {
_kc.init({
onLoad: 'check-sso',
silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html',
pkceMethod: 'S256',
})
.then((authenticated) => {
if (authenticated) {
onAuthenticatedCallback();
} else {
doLogin();
}
})
};
const doLogin = _kc.login;
const doLogout = _kc.logout;
const getToken = () => _kc.token;
const isLoggedIn = () => !!_kc.token;
const updateToken = (successCallback) =>
_kc.updateToken(5)
.then(successCallback)
.catch(doLogin);
const getUsername = () => _kc.tokenParsed?.preferred_username;
const hasRole = (roles) => roles.some((role) => _kc.hasRealmRole(role));
const UserService = {
initKeycloak,
doLogin,
doLogout,
isLoggedIn,
getToken,
updateToken,
getUsername,
hasRole,
};
export default UserService;
And then use the index to initiate this keycloak service and pass the render of your application as a call back.
import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";
import HttpService from "./services/HttpService";
import UserService from "./services/UserService";
const renderApp = () => ReactDOM.render(<App/>, document.getElementById("app"));
UserService.initKeycloak(renderApp);
HttpService.configure();

Related

Next.js: prevent Showing index page before redirecting to Keycloak Login page

I've been working on Next.js project which authenticates with a Keycloak.
I used '#react-keycloak/ssr' library to implement frontend authentication.
Here's my _app.js code (Code Reference: https://github.com/react-keycloak/react-keycloak-examples/tree/master/examples/nextjs-app)
import cookie from 'cookie'
import * as React from 'react'
import type { IncomingMessage } from 'http'
import type { AppProps, AppContext } from 'next/app'
import { SSRKeycloakProvider, SSRCookies } from '#react-keycloak/ssr'
const keycloakCfg = {
url: 'http://myauthurl/auth',
realm: 'myrealm',
clientId: 'myclientid',
}
interface InitialProps {
cookies: unknown
}
function MyApp({ Component, pageProps, cookies }: AppProps & InitialProps) {
const initOptions = {
onLoad: 'login-required',
checkLoginIframe: false
}
return (
<SSRKeycloakProvider
keycloakConfig={keycloakCfg}
persistor={SSRCookies(cookies)}
initOptions={initOptions}
>
<Component {...pageProps} />
</SSRKeycloakProvider>
)
}
function parseCookies(req?: IncomingMessage) {
if (!req || !req.headers) {
return {}
}
return cookie.parse(req.headers.cookie || '')
}
MyApp.getInitialProps = async (context: AppContext) => {
// Extract cookies from AppContext
return {
cookies: parseCookies(context?.ctx?.req),
}
}
export default MyApp
My goal was to redirect unauthenticated users to keycloak login page. It worked by adding 'initOptions'. However, before redirecting, application shows index page for one second.
EDIT:
After writing code checks the state of authentication, I managed to hide components for users who are not logged in. However, after login success application shows blank page and keycloak is undefined.
import type { AppProps, AppContext } from "next/app";
import { SSRKeycloakProvider, useKeycloak } from "#react-keycloak/ssr";
import { Provider } from "urql";
import {
keycloakConfig,
initOptions,
getPersistor,
Keycloak,
} from "../libs/keycloak";
import { parseCookies } from "../libs/cookie";
import { useMemo } from "react";
import { createUrqlClient, ssrCache } from "../libs/urql";
interface Props extends AppProps {
cookies: unknown;
token?: string;
}
function MyApp({ Component, pageProps, cookies, token }: Props) {
const urqlClient = useMemo(() => createUrqlClient(token), [token]);
const {keycloak} = useKeycloak
console.log(keycloak) //undefined after login success
console.log(keycloak?.authenticated ) //undefined after login success
// SSR cache for urql
if (pageProps?.urqlState) {
ssrCache.restoreData(pageProps.urqlState);
}
return (
<SSRKeycloakProvider
keycloakConfig={keycloakConfig}
persistor={getPersistor(cookies)}
initOptions={initOptions}
>
<Provider value={urqlClient}>
{keycloak?.authenticated && <Component {...pageProps} /> }
</Provider>
</SSRKeycloakProvider>
);
}
MyApp.getInitialProps = async (context: AppContext) => {
const keycloak = Keycloak(context?.ctx?.req);
return {
cookies: parseCookies(context?.ctx?.req),
token: keycloak.token,
};
};
export default MyApp;
It has to wait for a split seconds since it needs to check for authentication.. what is your desire behavior?
You can use the useKeycloak hook in your page to show the behavior you want (e.g. Redirecting to login page....)
const IndexPage = () => {
const { keycloak } = useKeycloak<KeycloakInstance>()
const loggedinState = keycloak?.authenticated ? (
<span className="text-success">logged in</span>
) : (
<span className="text-danger">NOT logged in</span>
)
const welcomeMessage =
keycloak?.authenticated
? `Welcome back!`
: 'Welcome visitor. Please login to continue.'
return (
<Layout title="Home | Next.js + TypeScript Example">
<h1 className="mt-5">Hello Next.js + Keycloak 👋</h1>
<div className="mb-5 lead text-muted">
This is an example of a Next.js site using Keycloak.
</div>
<p>You are: {loggedinState}</p>
<p>{welcomeMessage}</p>
</Layout>
)
}
export default IndexPage
Simplified example from https://github.com/react-keycloak/react-keycloak-examples/blob/master/examples/nextjs-app/pages/index.tsx

Prevent msal-react from loading components multiple times

I'm not too far into the implementation and have it pretty bear bones and not refactored (and not great coding practices).
This is an Admin portal that uses the company's Azure Active Directory to validate users as opposed to username/password, etc. The flow is this:
User navigates to /admin.
useEffect() starts checking if user isAuthenticated.
If not, they are automatically redirected to login.
Once they have logged in, or are verified as already being isAuthenticated, they can now access components that are children of <AuthenticatedTemplate>.
The only child component currently is <Token>, which for testing purposes, just prints out the JWT token returned from the API. Essentially what happens here is:
The accessToken, idToken, oid are automatically sent to my API for validation server-side as well.
When they are validated, the user is checked against the DB... if they exist, send a JWT... if not, add them and send a JWT.
JWT is saved to sessionStorage and is used subsequently for client-API communication.
The problem I'm running into my API is getting queried four or more times:
This is what I have so far:
// index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
import { PublicClientApplication } from '#azure/msal-browser';
import { MsalProvider } from '#azure/msal-react';
import { msalConfig } from './utils/authConfig';
// Instantiate MSAL that encapsulates the entire app
const msalInstance = new PublicClientApplication(msalConfig);
ReactDOM.render(
<React.StrictMode>
<MsalProvider instance={msalInstance}>
<App />
</MsalProvider>
</React.StrictMode>,
document.getElementById('root')
);
// App.tsx
import React, { useEffect } from 'react';
import {
AuthenticatedTemplate,
useMsal,
useAccount,
useIsAuthenticated,
} from '#azure/msal-react';
import { graphConfig, loginRequest } from '../utils/authConfig';
import { InteractionStatus } from '#azure/msal-browser';
import axios from 'axios';
const Token = (props: any) => {
if (props.account) {
props.instance
.acquireTokenSilent({
...loginRequest,
account: props.account,
})
.then((response: any) => {
axios
.post('/api/v2/auth/aad_token/validate/', {
access_token: response.accessToken,
id_token: response.idToken,
oid: response.uniqueId,
})
.then((response) => {
console.log(response.data);
})
.catch((error) => {
console.log(error);
});
});
}
return <div>Test</div>;
};
const App = () => {
const isAuthenticated = useIsAuthenticated();
const { instance, inProgress, accounts } = useMsal();
const account = useAccount(accounts[0] || {});
useEffect(() => {
if (inProgress === InteractionStatus.None && !isAuthenticated) {
instance.loginRedirect(loginRequest);
}
});
return (
<div className='App'>
<AuthenticatedTemplate>
<Token account={account} instance={instance} />
</AuthenticatedTemplate>
</div>
);
};
export default App;
Suggestions for how to limit this?
This fixed it for me... forgot the dependency array:
const Token = () => {
const { accounts, instance } = useMsal();
const account = useAccount(accounts[0] || {});
useEffect(() => {
// Check if account object exists
if (account) {
// If it does then acquire an accessToken
instance
.acquireTokenSilent({
...loginRequest,
account: account,
})
.then((response: any) => {
axios
.post('/api/v2/auth/aad_token/validate/', {
access_token: response.accessToken,
id_token: response.idToken,
oid: response.uniqueId,
})
.then((response) => {
console.log(response.data);
})
.catch((error) => {
console.log(error);
});
});
}
}, []);
};

aws cognito - how to keep the id token refresh at the right time in frontend

Problem:
Every time when I log in, the id token which is obtained by Auth.signIn will be store in localStorage.
After I login, UI make requests which require Authorization(use id token),
but it failed every time.
I tried to copy the id token in localStorage and tried the same API request in Postman,
below error message shown.
the incoming token has expired
But When I reload the page, the request is sent successfully and receive ok response.
I am not sure whether it's because the token refreshing logic is not correct in my code.
I just put the token refreshing logic in App.js componentDidMount().
The logic is based on below post.
how handle refresh token service in AWS amplify-js
Can someone let me know what's wrong of my code?
Index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
//aws
import Amplify from 'aws-amplify';
import config from './config.json'
const Index = () => {
Amplify.configure({
Auth: {
mandatorySignId: true,
region: config.cognito.REGION,
userPoolId: config.cognito.USER_POOL_ID,
userPoolWebClientId: config.cognito.APP_CLIENT_ID
}
});
return(
<React.StrictMode>
<App/>
</React.StrictMode>
)
}
ReactDOM.render(
<Index />,
document.getElementById('root')
);
serviceWorker.unregister();
App.js
import React, { Component } from 'react';
import { Switch, Route } from 'react-router-dom';
import { Redirect } from 'react-router';
import { withRouter } from 'react-router-dom';
import config from './config.json'
//Screen
import Login from './screen/auth/Login'
import Drawer from './components/Drawer'
import { Auth } from 'aws-amplify';
const AmazonCognitoIdentity = require('amazon-cognito-identity-js');
const CognitoUserPool = AmazonCognitoIdentity.CognitoUserPool;
class App extends Component {
state = {
isAuthenticated: false,
isAuthenticating: true,
user: null
}
setAuthStatus = authenticated =>{
this.setState({isAuthenticated: authenticated})
}
setUser = user =>{
this.setState({ user: user})
}
handleLogout = async () =>{
try{
Auth.signOut();
this.setAuthStatus(false);
this.setUser(null)
localStorage.removeItem('jwtToken')
localStorage.removeItem('idToken')
this.props.history.push('/')
}catch(error){
console.log(error)
}
}
tokenRefresh(){
const poolData = {
UserPoolId : config.cognito.USER_POOL_ID, // Your user pool id here,
ClientId : config.cognito.APP_CLIENT_ID// Your client id here
};
const userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
const cognitoUser = userPool.getCurrentUser();
cognitoUser.getSession((err, session) =>{
const refresh_token = session.getRefreshToken();
cognitoUser.refreshSession(refresh_token, (refErr, refSession) => {
if (refErr) {
throw refErr;
}
else{
localStorage.setItem('jwtToken',refSession.idToken.jwtToken)
localStorage.setItem('idToken',JSON.stringify(refSession.idToken))
}
});
})
}
async componentDidMount(){
try{
const session = await Auth.currentSession();
this.setAuthStatus(true);
const user = await Auth.currentAuthenticatedUser();
this.setUser(user);
}catch(error){
console.log(error);
}
// check if the token need refresh
this.setState({isAuthenticating: false})
let getIdToken = localStorage.getItem('idToken');
if(getIdToken !== null){
let newDateTime = new Date().getTime()/1000;
const newTime = Math.trunc(newDateTime);
const splitToken = getIdToken.split(".");
const decodeToken = atob(splitToken[1]);
const tokenObj = JSON.parse(decodeToken);
const newTimeMin = ((newTime) + (5 * 60)); //adding 5min faster from current time
if(newTimeMin > tokenObj.exp){
this.tokenRefresh();
}
}
}
render(){
const authProps = {
isAuthenticated: this.state.isAuthenticated,
user: this.state.user,
setAuthStatus: this.setAuthStatus,
setUser: this.setUser
}
return (
!this.state.isAuthenticating &&
<React.Fragment>
{this.state.isAuthenticated ?
<Drawer props={this.props} auth={authProps} handleLogout={this.handleLogout} onThemeChange={this.props.onThemeChange} /> :
<Switch>
<Redirect exact from='/' to='/login'/>
<Route path='/login' render={(props)=> <Login {...props} auth={authProps}/>} />
</Switch>
}
</React.Fragment>
);
}
}
export default withRouter(App);
Login.js
import React, { useState } from 'react';
import TextField from '#material-ui/core/TextField';
import withStyles from '#material-ui/core/styles/withStyles';
import _ from 'lodash';
import { Auth } from "aws-amplify";
function Login(props) {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = async (event) => {
event.preventDefault();
const payload = {
"username": username,
"password": password
}
// aws login
try{
const signInResponse = await Auth.signIn(payload.username,payload.password)
console.log(signInResponse)
props.history.push("/home")
props.auth.setAuthStatus(true)
props.auth.setUser(signInResponse)
localStorage.setItem('jwtToken',signInResponse.signInUserSession.idToken.jwtToken)
localStorage.setItem('idToken',JSON.stringify(signInResponse.signInUserSession.idToken))
}catch(error){
console.log(error)
}
}
return(
<form onSubmit={handleSubmit}>
<TextField
name='username'
value="username"
...
/>
<TextField
name='password'
value="password"
...
/>
</form>
);
}
export default withStyles(styles)(Login);
Why do you want to refresh token yourself as AWS Amplify handle it for you?
The documentation states that:
When using Authentication with AWS Amplify, you don’t need to refresh
Amazon Cognito tokens manually. The tokens are automatically refreshed
by the library when necessary.
Amplify automatically tries to refresh if the access token has timed out (which happens after an hour). You configure the refresh token expiration in the Cognito User Pools console.
import { Auth } from 'aws-amplify';
Auth.currentSession()
.then(data => console.log(data))
.catch(err => console.log(err));
Auth.currentSession() returns a CognitoUserSession object which contains JWT accessToken, idToken, and refreshToken.
This method will automatically refresh the accessToken and idToken if tokens are expired and a valid refreshToken presented. So you can use this method to refresh the session if needed.
https://docs.amplify.aws/lib/auth/manageusers/q/platform/js#managing-security-tokens
https://docs.amplify.aws/lib/auth/manageusers/q/platform/js#retrieve-current-session
I built an app with the tutorial here: https://aws.amazon.com/getting-started/hands-on/build-serverless-web-app-lambda-apigateway-s3-dynamodb-cognito.
In the example application provided at s3://wildrydes-us-east-1/WebApplication/1_StaticWebHosting/website there is a file cognito-auth.js. In that file there is:
YourApp.authToken = new Promise(function fetchCurrentAuthToken(resolve, reject) {…
This does not refresh the auth token until the entire page is reloaded. I ran into issues when the page was loaded for a long time (hours) and only ajax calls were made. After the auth token that was loaded at the page load expired, the authorized ajax calls to my API returned 401 errors.
To resolve this, I added an async function wrapper to the promise:
YourApp.authToken = async function () {
return await new Promise(function fetchCurrentAuthToken(resolve, reject) {…
Then changed the app references from
YourApp.authToken.then(function (token) {…
to
YourApp.authToken().then(function (token) {…

Firebase Auth, Logged out on page refresh

Technologies: I'm using Firebase Auth with NextJS & React.
Problematic: Logged users can use the web app with firebase auth normally only if they navigate within the app via Next routing, whenever they refresh the page or open a new account tab they are not logged in anymore.
Issue: It is extremely frustrating because this problem only occurs on production. There's no problem at all on the staging & localhost environment.
firebase.js: Initialize firebase.
import getConfig from "next/config";
import * as firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/analytics';
const { publicRuntimeConfig } = getConfig();
export async function initializeFirebase() {
if (!firebase.apps.length) {
firebase.initializeApp(JSON.parse(publicRuntimeConfig.FIREBASE_CONFIG));
if (publicRuntimeConfig.FIREBASE_ANALYTICS) {
firebase.analytics();
}
}
}
export const auth = firebase.auth
export const db = firebase.firestore;
export default firebase;
AuthHoC.js: To make sure the user is connected I wrapper my pages with a HOC.
export default App => (
class AuthHoC extends App {
_isMounted = false;
constructor(props) {
super(props)
this.state = {
loading: false,
isVerified: false,
idToken: undefined,
isAuthenticated: false
}
}
async componentDidMount() {
this._isMounted = true;
await initializeFirebase();
// onAuthStateChanged return a function that we'll use to unsubscribe our listener
this.unsubscribeMethod = await auth().onAuthStateChanged(this._handleStateChange);
}
// is user is null, we're no longer authenticated
_handleStateChange = (user) => {
let that = this;
if (user) {
// NOT PASSING HERE ON PAGE REFRESH...
user.getIdToken().then(function(idToken) {
that.setState({
loading: true,
idToken: idToken,
isVerified: user.emailVerified,
isAuthenticated: !!user
});
});
} else {
...
}
}
componentWillUnmount() {
if (this.unsubscribeMethod) {
this.unsubscribeMethod();
}
this._isMounted = false;
}
render() {
return ( <>
{this.state.loading ?
<App {...this.props} {...this.state} />
:
... loading ...
}
</> )
}
});
_app.js: (NextJS) Wrap every pages with the Higher Order Component.
import App from "next/app";
import AuthHoC from '../utils/authentication/authHoC';
class MyApp extends App {
render() {
const { Component, pageProps, isAuthenticated, idToken, isVerified } = this.props;
return (
<Component
{...pageProps}
isAuth={isAuthenticated}
idToken={idToken}
isVerified={isVerified}
/>
);
}
}
export default AuthHoC(MyApp);
What could be the issue? All these codes work on localhost & staging url, just not on production.
EDIT:
I pinpointed the problem, I just switched my production keys with staging and it works, that means that the problem is not coming from the Heroku or my code but my Firebase configuration itself.
The main difference is that the prod use analytics. If you have any info I forgot to configure any suggestion would help.

Amplify+react: hosted ui with federated identity provider

I would like to use the hosted ui with amplify in react.
The authentication should be done using federated identity provider, which is working correctly.
But I don't know, how to write the react part.
I found in amplify tutorial: that is should be possible with this sample:
// OAuthButton.js
import { withOAuth } from 'aws-amplify-react';
import React, { Component } from 'react';
class OAuthButton extends Component {
render() {
return (
<button onClick={this.props.OAuthSignIn}>
Sign in with provider's account
</button>
)
}
}
export default withOAuth(OAuthButton);
// App.js
import React, { Component } from 'react';
import './App.css';
import OAuthButton from './OAuthButton';
import Amplify, { Auth, Hub } from 'aws-amplify';
import awsconfig from './aws-exports'; // your Amplify configuration
// your Cognito Hosted UI configuration
const oauth = {
domain: 'your_cognito_domain',
scope: ['phone', 'email', 'profile', 'openid', 'aws.cognito.signin.user.admin'],
redirectSignIn: 'http://localhost:3000/',
redirectSignOut: 'http://localhost:3000/',
responseType: 'code' // or 'token', note that REFRESH token will only be generated when the responseType is code
};
Amplify.configure(awsconfig);
Auth.configure({ oauth });
class App extends Component {
constructor(props) {
super(props);
this.signOut = this.signOut.bind(this);
// let the Hub module listen on Auth events
Hub.listen('auth', (data) => {
switch (data.payload.event) {
case 'signIn':
this.setState({authState: 'signedIn', authData: data.payload.data});
break;
case 'signIn_failure':
this.setState({authState: 'signIn', authData: null, authError: data.payload.data});
break;
default:
break;
}
});
this.state = {
authState: 'loading',
authData: null,
authError: null
}
}
componentDidMount() {
console.log('on component mount');
// check the current user when the App component is loaded
Auth.currentAuthenticatedUser().then(user => {
console.log(user);
this.setState({authState: 'signedIn'});
}).catch(e => {
console.log(e);
this.setState({authState: 'signIn'});
});
}
signOut() {
Auth.signOut().then(() => {
this.setState({authState: 'signIn'});
}).catch(e => {
console.log(e);
});
}
render() {
const { authState } = this.state;
return (
<div className="App">
{authState === 'loading' && (<div>loading...</div>)}
{authState === 'signIn' && <OAuthButton/>}
{authState === 'signedIn' && <button onClick={this.signOut}>Sign out</button>}
</div>
);
}
}
export default App;
In this form it is not working.
QA:
how it should looks like the OAuthSignIn method?
Because when I used the window.open method to redirect to the hosted ui url, my react app doesn't know that the user was authenticated, how the code should looks like, that the app could recognize that the login flow was finished successfully?

Resources