AWS Cognito: Restrict users to a one login at a time - amazon-cognito

Is there any way to restrict users to a single [simultaneous] session?
I'd like to be able to check if the current user already has a session. If they do, then they can opt to sign that session out, before continuing.
To be clear, at the moment it is possible to login the same user from multiple browser tabs, (from the same cognito application)
cognitoUser.authenticateUser(authenticationDetails, {
cognitoUser: this.cognitoUser,
onSuccess: (result) => {
this.userSession = result
console.log('successfully logged in', result)
// But are they already logged in somewhere else?
},
onFailure: (error) => {
console.log('Login failed for some reason...')
callback(error)
}
}
I understand that Cognito is built with mobiles/apps in mind so this might not be possible without using a login lambda hook... ? Even then I'm not sure if it's possible without maintaining a table of logged in users...?!

You can signs the current user out globally from all the devices by invalidating all issued tokens
cognitoUser.globalSignOut();
or signs the current user out from the application in existing session in the browser.
if (cognitoUser != null) {
cognitoUser.signOut();
}
You can onvoke either of the above just before user sigins in back again using the login screen.

I used the admin API - "AdminUserGlobalSignOut" for invalidating all the session tokens provided to the user before I send the login request from my lambda to Cognito.
You can refer to this link for official docs from aws.
https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminUserGlobalSignOut.html
Pro Tip - AdminUserGlobalSignOut endpoint kills the refresh token
validity not the Id Token validity - so if the creds are shared at
12pm and the user signs in using another device in 5 mins or so - the
first device ID token login will still work on the first device until
its not expired - by default ID Token is valid for 1 hour.

Related

Clarification on Google Authentication and Authorization, with OAuth in 2022

I am writing an App and am trying to leverage Google for A&A. The app itself relies on access to the users Google Calendar, and so initially I leveraged their updated OAUTH2 library for A&A.
Here is my flow:
User goes to the index.html which has "https://accounts.google.com/gsi/client" script and google.accounts.oauth2.initCodeClient is called with my client_id, scopes, redirect url
<script src="https://accounts.google.com/gsi/client"></script>
<script>
let client;
function initClient() {
client = google.accounts.oauth2.initCodeClient({
client_id: 'xxxxx-xxxx.apps.googleusercontent.com',
scope:
'https://www.googleapis.com/auth/userinfo.profile \
https://www.googleapis.com/auth/userinfo.email \
https://www.googleapis.com/auth/calendar.readonly \
https://www.googleapis.com/auth/calendar.events',
ux_mode: 'redirect',
redirect_uri: 'http://localhost:5000/oauth2callback',
});
}
// Request an access token
function getAuthCode() {
client.requestCode();
}
The user clicks the login button, which kicks off requestCode() and they begin the login flow. They login or select their google account, then besides the unapproved app screen, they get to the consent screen with my requested scopes.
After, they are redirected to my expressjs endpoint and using the "googleapis" library I exchange with id_token for the access and refresh tokens.
...
const { tokens } = await oauth2Client.getToken(req.query.code); //exchange code for tokens
const userInfo = (
await oauth2Client.verifyIdToken({
idToken: tokens.id_token,
audience: config.google.clientID,
})
).payload;
if (!indexBy.email[userInfo.email]) { // check if user exists
const newUser = {
name: userInfo.name,
email: userInfo.email,
o_id: userInfo.sub,
picture: userInfo.picture,
r_token: tokens.refresh_token,
};
...
Ok, all good.... but not quite. The problem is, that next time the user wants to login to the app, they go through the entire flow again, including the consent screen (again).
So, after going through more docs, even looking at examples from google. I was surprised and I noticed that many of those apps used the passport oauth2 plugin :( Something i've done in the past, but was hoping to avoid that with the recently updated Google client and nodejs libraries.
Ok, how to not prompt for consent screen on subsequent logins?
Maybe separate A&A, so first I use "Sign In With Google" for Authentication, then when I get the user info, check if the user is already registered (hence I have already saved the refresh token) and they start the app.
On the other hand, if they are new (not in existing app user collection), after authenticating, I will then call the OAUTH2 authorization redirect, so again they on Googles site, this time to do the scopes api confirmation.
So, first question, is that the best practice with most apps with leverage a Google API via OAuth? To first Authenticate, then possibility Authorize (as needed). Hopefully this will still work ok when things come up with expired/invalid refresh token (fingers crossed the default google library handles that).
When doing the Authorize for consent, can I pass something from the previous Authenticate flow so they don't need to do that again.
Or maybe when doing the Authenticate process (Google Identity Service), there is some flag or param so that if they have already consented, they don't have to do that again on subsequent logins.
Incase I wasn't clear, in a nutshell the question is: should I be doing Authenticate for login, separately from Authorization (oauth2 token). Or should I go right into the Authorization flow, which first Authenticates the user, and can I skip the Authorization consent screens if they've already done that. Or maybe there's another way which is the best practice.
Thanks for your attention.
Background info
Authentication is the act where by a user logs in into a system using their login and password. With authentication we know that the user is behind the machine. For this we use Open id connect, which was built on top of Oauth2. Open id connect returns and id_token which can be used to identify the user, it is often a jwt containing some claims to identify the subject or the user behind the Authentication.
The scope used for open id connect is profile and email. open id connect grants you consent to access a users profile information.
This is an example of the decrypted id token returned by google from a simple call using profile scope only. All this id token is telling you is who the user behind the machine is.
{
"iss": "https://accounts.google.com",
"azp": "4074087181.apps.googleusercontent.com",
"aud": "4074087181.apps.googleusercontent.com",
"sub": "1172004755672775346",
"at_hash": "pYlH4icaIx8PssR32_4qWQ",
"name": "Linda Lawton",
"picture": "https://lh3.googleusercontent.com/a-/AOh14GhroCYJp2P9xeYeYk1npchBPK-zbtTxzNQo0WAHI20=s96-c",
"given_name": "Linda",
"family_name": "Lawton",
"locale": "en",
"iat": 1655219027,
"exp": 1655222627
}
In the same call google also returned an access token. Now my call contained only the scope for profile, due to the fact that its open id connect. This means that I will only have access to the data that the profile scope would grant access to. In this case most of what is behind the Google people api.
Note: The user does not see a consent screen with open id connect, even though they are consenting to profile scope. It is assumed by signing into your account that the system you are logging into would have access to your profile info.
Authorization
Authorization is the process by which a user grants your application authorization to access their private user data. The user is shown a consent screen where they consent to your application accessing data defined by some scopes.
In the case of google calendar api there are serval
https://www.googleapis.com/auth/calendar See, edit, share, and permanently delete all the calendars you can access using Google Calendar
https://www.googleapis.com/auth/calendar.events View and edit events on all your calendars
https://www.googleapis.com/auth/calendar.events.readonly View events on all your calendars
https://www.googleapis.com/auth/calendar.readonly See and download any calendar you can access using your Google Calendar
https://www.googleapis.com/auth/calendar.settings.readonly View your Calendar settings
In this case you are only given an access token this is again Oauth2 it is authorization to access the users calendar data it is not authentication this is not related to login.
Your question
So, first question, is that the best practice with most apps with leverage a Google API via OAuth? To first Authenticate, then possibility Authorize (as needed).
You would do both at the same time.
When you authencation your user make sure to include your google calendar scope then the access token and refresh token returned will grant you access to google calendar.
I am going to assume that you have some kind of user system. When you store the user be sure to store the refresh token that is returned.
As far as Authentication goes i will assume you either have a remember me system which will set a cookie on their machine and remember the user so that you can then get the refresh token from their system the next time they come back.
If they did not chose to select a remember me option then will then have to login every time they visit your site but part of the login will return the "sub": "1172004755672775346", this is the users id on google system so you can use that in your database to match the user when they come back.
Your question is quite complex and will depend upon the type of system you have what it is designed to do as well as what programming language you are using. That being said I hope this very long answer clears things up a bit.

Supertokens - revoke another user's session without signing him off

I am using supertokens for authetication. Upon changing another user's permissions, I revoke his session which will cause his role to be updated after his current active token runs it's lifetime limit.
This causes the other user to be logged off at that point.
I would like his role to be updated (i.e. his session re created), but without logging him out and asking him again for his credentials.
Is that possible?
I will answer this question assuming that you are using the NodeJS SDK. If not, the answer overall still applies, but the function's name will change.
What you want is possible. I assume that you are strong that user's role in the access token, so instead of revoking the other user's session, you should use the updateAccessTokenPayload function from the SDK:
import Session from "supertokens-node/recipe/session";
async function updateRoleOfOtherUser(userId: string) {
// we first get all the sessionHandles (string[]) for a user
let sessionHandles = await Session.getAllSessionHandlesForUser(userId);
// we update all the session's Access Token payloads for this user
sessionHandles.forEach(async (handle) => {
let currAccessTokenPayload = (await Session.getSessionInformation(handle)).accessTokenPayload;
await Session.updateAccessTokenPayload(handle,
{ role: "newRole", ...currAccessTokenPayload }
);
})
}
The update the role will take into affect when their session refreshes, and they won't be logged out.
If you want the update to take affect immediately, you can maintain a cache on your side marking all the session handles that need to be refreshed early. On successful verification of of any such session that contains those session handles, you can send a 401 to the frontend forcing a session refresh and causing an update in their role.

Persistent User Session on React Native App

I have already implemented a user authentication, in which the user can log in again or register.
I would now like to avoid that the user must register again after each closing of the app. For this reason I have to request a refresh token at certain intervals (Max 1 hour, as long as the cookie is valid). my question: how do I do that best? the refresh should work for both open and closed apps. I saw the possibility of the React Native Background task, but apparently only runs when the app is closed.
You have to create a flag in AsyncStorage when the user is authenticated.
And then you have to check that flag each time on opening the app.
Here is the sample snippet.
AsyncStorage.setItem('loggedIn', true);
And in your app.js you can check this flag in constructor
AsyncStorage.getItem('loggedIn').then((value) => {
if (value) {
//user logged in logic goes here
} else {
// user logged out. You need to login him again
}
});
Considering that you need to persist user's login forever and still be able to refresh the token, one possible way is to store the user's credentials (username and password) in some secure storage like react-native-keychain and then refresh the token every time the user opens the app.
Or more precisely, automate the login with the credentials you stored whenever the user launches the app with the help of useEffect hook(componentDidMount).
Note: This is not a good implementation if your app uses push notifications. Push notifications demand the user to be authorized all the time.

Cannot reset password for users created in cognito console

Im creating a login for a CMS to a website for a small business using the React javascript framework. Obviously random people can't just register their own details so at present i am attempting to manually create the users (a max of 5 at present) within the cognito console. I have the majority of the authentication workflow established (using just the Auth library from Amplify) with INITIAL forced password reset, forced MFA TOTP authentication and successful access to the CMS on login. However the Forgotten Password functionality despite my best efforts just refuses to work and I believe I've narrowed it down to cognito itself.
Short of trying every damn combination of a user pool and created user to get this to work, I provide the following when creating a user from the cognito console -
1. Username (must be email)
2. Temporary Password
3. No phone or phone verification
4. Email and tick email verification
For the userpool conditions the following is the current working setup (minus the forgotten password functionality)
1. Email Address required for sign in
2. Standard attributes are given name, family name, phone number
3. MFA is required
4. Second factor is Time Based One Time Password
5. Email is selected as the attribute to be verified (which i believe to be a moot point as this is manually done when creating user)
6. No SMS role provided, required or necessary
7. I have verified an email address with SNS and have this entered in the FROM and REPLY-TO fields for email customization and have selected the use of using Amazon SES below these customization fields.
This is the entry point of my Authentication workflow, identifying where in the workflow the user is at and acting accordingly.
await Auth.signIn(email, password)
.then(user => {
setFetching(false);
switch (user.challengeName) {
case "NEW_PASSWORD_REQUIRED":
switchComponent("Verify", user);
break;
case "SMS_MFA":
case "SOFTWARE_TOKEN_MFA":
switchComponent("ConfirmSignIn", user);
break;
case "MFA_SETUP":
switchComponent("MFASetup", user);
break;
default:
history.push({ pathname: "/" });
break;
}
})
Everything works as it should for the most part. MFA workflow displays a nice QR Code for the user to utilize and confirm using their Authenticator of choice, NEW_PASSWORD_REQUIRED is submitted via the following -
const handleSubmit = async event => {
event.preventDefault();
if (noErrors()) {
setFetching(true);
await Auth.completeNewPassword(inputs.user, inputs.newPassword, {
email: inputs.email,
phone_number: inputs.phoneNumber,
given_name: inputs.givenName,
family_name: inputs.familyName
})
.then(() => {
setFetching(false);
switchComponent("MFASetup", inputs.user);
})
.catch(err => onShowDialog(err.message));
setFetching(false);
}
};
From what i can tell, nothing is out of the ordinary here. However any attempts to initialize the forgotten password flow after successfully authenticating past the REQUIRE_PASSWORD_RESET, even from the cognito console and i am presented with "Cannot reset password for the user as their is no registered/verified email or phone number", this is despite enabling the "verified email" when creating the user from the cognito console.
By using the aws command line I can force the verification however this to me is just infuriatingly unintuitive when the enabling of this when creating the user should take effect. Im at my wits end here and I have clients waiting for this software. Any help would be greatly appreciated in this instance. I apologize for any redundant content in this question I just want to make sure I cover everything the first time. Regards.

Disable Multiple login at Keycloak

It is possible to disable multiple login at Keycloak?
Example:
I logged in on my browser at my PC and I do a login on my mobile phone... at this moment, keycloak invalidade the token of my other browser.
It is possible?
Update:
This feature has been implemented in keycloak version 17.
See: https://github.com/keycloak/keycloak/issues/10077
Old workaround:
Keycloak does not have this feature implemented yet. If you want you can create your own custom authenticator, for that you should have sound knowledge of keycloak codebase. I was facing the same issue and came up with a workaround.
Keycloak maintains active sessions of a user as shown below:
Every time a user logs in through different device, a session is added in the above list, we can use the above info to limit the user session to one.
I'm heavily making use of keycloak admin rest APIs to get the session-related data and to delete the session: link here
Make an API request to your server when a user logs in:
client.js
componentDidMount(){
let auth = await keycloak.init({onLoad: "check-sso"});
if(auth){
const {data} = await axios.get(`/check_session/${keycloak.subject}`);
this.setState({isSessionValid: data.isSessionValid})
}
}
render(){
return(
{this.state.isSessionValid ?
<YourComponent/> :
<div> You are already logged in!</div>
}
)
}
server.js
const baseUrl = 'http://localhost/auth';
const realm = 'myRealm'
// check session of user with same credentials from different devices
export const checkUserSession = async userId => {
// initialize access token
await initializeAccessToken(baseUrl); // check admin rest api docs of keycloak
// get all active sessions of user
const sessions = await getUserActiveSessions(baseUrl, realm, userId); // check admin rest api docs of keycloak
// check if any session exists
if (sessions && sessions.length > 1) {
// sort with start time in descending order
sessions.sort((a, b) => b.start - a.start);
const currentSession = sessions[0]; // current logged in user's session
const previousSession = sessions[1]; // previously active user's session
/* I'm preventing current user to log in, for your case you can delete
the old session of this user here */
await deleteUserSession(baseUrl, realm, currentSession.id); // check admin rest api docs of keycloak
return { isSessionValid: false, sessionData:previousSession };
}
// user logged in first time
return { isSessionValid: true };
};
Looks like a lot of Keycloak users are interested in this feature so I am posting an answer which I found on my conducted research, this feature implemented by unofficial developer and here is what he shared
You can get the authenticators from my cloned branch:
https://github.com/mfdewit/keycloak/tree/KEYCLOAK-849-configurable-session-limits/services/src/main/java/org/keycloak/authentication/authenticators/sessionlimits
I've created 2 authenticators:
`RealmSessionLimitsAuthenticator`: limits the total number of sessions for a realm.
`UserSessionLimitsAuthenticator`: limits the number of session per user account and per client.
For now I advise you to copy them (including the factories) to your own project and make sure they are referenced in META-INF/services.
Also, change the AuthenticationFlowError.SESSION_LIMIT_EXCEEDED error to some value that exists in your Keycloak version. SESSION_LIMIT_EXCEEDED is added by me and only exists on my feature branch.
The downside is that you cannot provide the user proper feedback in case his session is denied.
If you have any questions on how these should be used, let me know!
Regarding the status of this task: I still have to write the integration tests before I can submit the merge request. But I'm planning to finish this soon. If anyone want to help writing these tests, I'd be grateful.
Still keycloak official JIRA i didn't find if they incorporated this feature in any recent version of Keycloak
Source
https://github.com/mfdewit/keycloak/tree/KEYCLOAK-849-configurable-session-limits/services/src/main/java/org/keycloak/authentication/authenticators/sessionlimits
These feature has been implemented in keycloak 17 : https://github.com/keycloak/keycloak/issues/10077