Keycloak js not executing THEN after init - javascript

I am trying to integrate Keycloak login into my React app and I'm trying to get the JWT from keycloak. Here is the code:
const [keycloakState, setKeycloakState] = useState<any>();
const login = () => {
const keycloak = Keycloak("/keycloak.json");
keycloak.init({onLoad: 'login-required'}).then(authenticated => {
console.log('kk', keycloak)
console.log('at', authenticated)
setKeycloakState({ keycloak: keycloak, authenticated: authenticated });
}).catch(err => {
alert(err);
});
console.log('log after')
}
The login function is triggered when a button is clicked. It redirects properly to keycloak, I can log in and I am properly redirected to the app. The problem is that after the redirect back to the app with proper login the code in the then part of the chain is not executed, and even the 'log after' does not appear in the logs. The catch error part works fine.
Why might this be happening? I have keycloak-js added to my project.

I used to face this problem before. The way that I passed is separating the "init" function and then invoke it later.
Here is my example on jsFiddle: 'https://jsfiddle.net/gzq6j3yu/1/'

Our solution was to use the functions onAuthSuccess and onAuthError avaliable on the KeycloakInstance keycloak-js provides. (The documentation around this is a little shaky but you can find them if you check out the source code.) As the names imply these functions get called when an auth attempt is successful or unsuccessful, respectively.
note: in the following snippets this._instance replaces OP's keycloak constant.
Basic code snippet:
import Keycloak from 'keycloak-js';
...
// pulled from a class's init function from a custom Keycloak helper class so won't translate one for one but you get the point.
this._instance = Keycloak(configObject);
this._instance.onAuthSuccess = () => {
// code to execute on auth success
};
this._instance.onAuthError = () => {
// code to execute on auth error
};
this._instance.init(initOptions)
...
We also had a getter to get the token on the KeycloakInstance (or empty string) on the same class. This is an easy way to refer to the token in your code to check if it actually exists, etc. Here's what that'd look like inside the class.
get token() {
return this._instance ? this._instance.token : '';
}
Hope this can help out some folks.

I think the reason your fulfilled callback is not executed is the way your app interacts with Keycloak. You initialize the Keycloak-Adapter with onLoad: 'login-required' which will redirect the user to Keycloak - which means the Javascript execution is interrupted at this point. Keycloak will redirect the user back to your app and since you wrapped the Keycloak-Adapter in a function which is only executed when a button is clicked, the promise callback is not executed.
Simple example:
// do this on page load
keycloak.init({onLoad: 'login-required'}).then((authenticated) => {
console.log('authenticated', authenticated)
})
You will not see a "authenticated", false in your console when you open up your app. If the user is not authenticated, he will be redirected (so no chance to execute that callback). If he then comes back and is authenticated, the callback will be executed and authenticated should be true.
If you want the user to click a button, a setup like this should work:
// do this somewhere early in your App or main.js and in a way that this code is executed on page load
const keycloak = new Keycloak(configuration);
keycloak.init({onLoad: 'check-sso'}).then((authenticated) => {
if (authenticated) {
// do what needs to be done if sign in was successful, for example store an access token
} else {
// handle failed authentication
}
}).catch(err => {
alert(err);
});
const login = () => { // this could be an on-click event handler
keycloak.login();
};
check-sso won't redirect the user to Keycloak if unauthenticated, so the user can trigger the login when needed.
Keep in mind that your JavaScript code will run twice and you have to cover both cases, first the user is not authenticated and needs to be redirected to Keycloak and a second time once the user comes back from Keycloak (then we should get the information that the user is authenticated in .then().

Related

useEffect with function that update the state as dependency leads to infinite loop

Code Sandbox: https://codesandbox.io/s/new-breeze-d260k
The code:
This is a simple auth app
When you click Login, the accessToken is exchanged and stored in memory while the refreshToken is stored in localStorage.
While the accessToken is valid (here a timestamp), the Home page shows the protected content
At every page reload (i.e App initialization), the refreshToken is sent to the server and if it is valid, a new accessToken is exchanged.
The problem:
To refresh the token on App initialization, I have an onRefreshToken() function in a useEffect to be executed once (I wanted to pass an empty array as dependency but typescript/eslint complains and suggest that onRefreshToken() should be the dependency. I admit that I don't understand why this is recommended to have always a dependency when you want the effect to be executed once).
Once the token is renewed, I store the accessToken and the user profile in their respective context.
Infinite re-render loop begins. On my local server, this is due to setProfile() and not setAccessToken(). However I don't understand why.
Side note
The above issue is the main issue of this post but on a side note, the login/logout process don't sync between tabs so if you have any idea why, I would be happy to hear your advice on this point as well.
Happy new year
One way to fix this would be to check to see if you have an access token and only refresh it if you need to:
export default function App() {
const { accessToken } = useAuthContext();
const { onRefreshToken, onSyncLogin, onSyncLogout } = useAuth();
useEffect(() => {
const refresh = async () => {
await onRefreshToken();
};
!accessToken && refresh();
}, [onRefreshToken, accessToken]);

Using keycloak in Vue3, but not on startup

I have a problem with keycloak.js in my Vue3 application
const keycloak = Keycloak(keycloakOptions)
keycloak.init({
onLoad: 'login-required',
checkLoginIframe: false
}).then(async (auth) => {
if (!auth) {
window.location.reload()
} else {
const app = createApp(App)
app.provide(process.env.VUE_APP_KEYCLOAK_PROVIDE_VARIABLE, keycloak)
app.use(store)
await store.dispatch('keycloakStore/fillRoles', keycloak.realmAccess.roles)
app.use(router)
app.mount('#app')
}
}).catch((e) => {
console.log('Serwer lezy: ' + e)
})
I have the above code in my main.js file. This runs the keycloak subpage with login/register. If someone succesfully logs in, my Vue app starts. Now, instead of the above, I’d like to make it so the Vue app starts regardless of the person being logged in and the keycloak screen is ONLY launched if someone clicks a specified button in the UI. I’ve searched examples on the net, but i can only find ones that run keycloak on application start. Can someone help?
Turn onLoad value from 'login-required' to 'check-sso'. This option will only verify if the user is logged in without redirecting the user to login, and setup authentication properties acordingly.
Then you can call keycloak.login(), for example in your code. Together with route guards if necessary.
Also, put the creation of the app out of the else block, because it will not load if the user is not logged in when you have 'check-sso'.

why is my firebase authentication display name not getting updated properly with react js?

I am trying to build firebase authentication for my react app..After I sign in I am trying to update the displayName and then redirect..On the redirected page I am trying to greet the user by fetching the display name saved while signing up with firebase..This page works properly immediately after I redirect but if I reload this page then it is not able to show the displayName and throws this error:
TypeError: Cannot read property 'displayName' of null
This is the function which gets triggered when signup button is clicked..
const signup = async () => {
try{
await firebaseApp.auth().createUserWithEmailAndPassword(email, password)
await firebaseApp.auth().currentUser.updateProfile({displayName:username})
console.log(firebaseApp.auth().currentUser)
if (!firebaseApp.auth().currentUser){
setLoading(true)
}
history.push('/home')
}catch (error){
alert(error.message)
}
}
This is the JSX of the page which is being redirected to by signup page:
<div className="greetings">
Good Evening {firebaseApp.auth().currentUser.displayName}
</div>
Why is this issue happening and how to resolve it?
firebaseApp.auth().currentUser is always null when a page first loads. It won't contain a User object until some time later, after the SDK is able to load and verify the auth token for that user. Instead of using currentUser, you should set up an auth state observer as shown in the documentation. This observer will get invoked as soon as the User object is known.
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
// User is signed in.
// ...
} else {
// User is signed out.
// ...
}
});
You can use the results of this observer function to know when the user is signed in or signed out over time. To learn more about how it works, read this blog post.

Firebase Custom Claims doesn't propagate

I'm working in an Angular6 app with angularfire2. I'm setting the roles as custom claims in user creation, but it doesn't seem to propagate.
When I'm creating the user I send the userid, businessid and role to a cloud function:
bid > businessid
urole > role
req.body.uid > userid
const customClaims = {
roles: { [bid]: urole }
}
admin.auth().setCustomUserClaims(req.body.uid, customClaims)
.then(result => {
res
.status(200)
.send()
})
The problem is when the call to cloud function finishes and I want to redirect the user to a route which requires the user to have the custom claim set, but it fails. After some debugging, I've found out that if run:
this.angularFireAuth.auth.currentUser.getIdTokenResult(true).then(result => {
return result.claims.roles
})
immediately after the call to the cloud function "result.claims.roles" is undefined, but if I refresh the page, "result.claims.roles" have the data I set before.
I've already tried the reload method, and getIdToken(true) but I'm getting the same problem.
Is there a way to avoid refreshing the page and get the custom claims?
Thank you!
When the user is signed in, they get an ID token that is valid for about an hour. If you set a custom claim, their (server-side) profile is updated immediately, but their ID token is not auto-updated. So you'll need to refresh their ID token to get the new custom claims.
As far as I know this ID token is only refreshed by calling getIdTokenResult if it has expired. If that's the cause, calling user.reload() and then getting the ID token should give you the updated claims.
For me it simply worked taking the advice from one of the comments:
// --------
// Frontend
// --------
// Triggering the cloud function
const url: string = 'url-to-your-cloud-function'
await this.http.post<unknown>(url, {}).toPromise();
// After cloud function was run and custom claim was set -> refresh the id token
// The 'currentUser' is a reference to the firebase user
await this.authService.currentUser.getIdToken(true);
// --------
// Cloud Function - createSubscription
// --------
const createSubscription = () => {
await admin.auth().setCustomUserClaims(userId, {
subscriber: true
})
}

Authentication logic for sessions that last while tab is open

Assume you are working on a front end application that performs authentication through 3rd party api. Successful authentication returns a json web token.
What would be best practices to store such token and create some sort of session for user while he is active on the website i.e. didn't close a tab or browser, however refreshing / reloading a page should not destroy such session.
Also, how can this session be used to protect routes? I am working with a stack consisting of react / redux / node / express and quiet a few other libraries. I believe I can perform certain checks within my react-router, however wouldn't it be better to do these on the express side?
You can store the token in localStorage or sessionStorage, and include it in every API request.
Local storage outlives the tab, it's stored there until you explicitly delete from it, so refreshing a page won't be a problem. Even closing a tab and then coming back won't be.
Session storage allows you to store data. Page refreshes are fine, but tab closing isn't, which is closer to the behavior you want.
As for protecting routes, the server should obviously check the token on requests to all protected API routes.
On the browser side, you will probably want to show a login form if a user tries to visit a protected route but the token isn't there (or is invalid).
With react-router, you could do it like the official repo shows in the example, via onEnter hooks: https://github.com/reactjs/react-router/blob/master/examples/auth-flow/app.js
An alternative would be to create two top-level components, one for protected routes, one for public routes (like a landing page or the sign in/sign up forms). The protected handler will then in componentWillMount check if there's a token:
- PublicHandler
+ SignIn
+ SignUp
+ Index
- ProtectedHandler
+ Dashboard
+ MoneyWithdrawal
it may looks like that , with sessionStorage (JWT token is accesseble, untill browser or tab closed)
///action creator redux
export const signupUser = creds => dispatch =>{
dispatch(requestSignup());
return API.auth.signup(creds)
.then(res => {
sessionStorage.setItem('token', res.token);// <------------------
dispatch(receiveSignup(res));
return res;
})
.catch(err => {
dispatch(SignupError(err));
);
});
};
On client : handling auth through HOC redux-auth-wrapper
On server on server you can use passport-jwt strategy
passport.use('jwt',new JwtStrategy(opts, function(jwt_payload, done) {
User.findOne({where:{ id: jwt_payload.user.id }}).then(user=>{
if (user) {
done(null, jwt_payload.user);
} else {
done(null, false);
// or you could create a new account
}
},err=>{
console.log('Error ',err);
return done(err,false);
});
}));
then just add route handler
var checkJWT = passport.authenticate('jwt')
router.get('/protected',checkJWT, (req, res) =>{
res.json(req.user);
});
You don't need sessions on server for that

Categories

Resources