Google SSO not asking for scope for just one user (other users OK) - google-oauth

I'm seeing a strange problem with google SSO and one of my users.
I'm requesting the following scopes using the oauth:
const url = oauth2Client.generateAuthUrl({
access_type: 'offline', // we want refresh tokens
scope: ['email'],
state: '{...}', //state object
prompt: 'consent',
hd: 'example.com', // restrict to just our domain
include_granted_scopes: true,
});
For every other user redirected to url, they are given a consent screen after authenticating that looks like this:
EXCEPT for one user. For this user - when they authenticate, there is no consent screen, and they are logged in with the bare minimum scope.
When the user looks at their account security page, they see our webapp as authorized with no scope. I've asked the user to revoke the app permissions and to try logging in again, but the same thing happens. They are logged in with no consent screen and no scope. Again, the strange thing is that it is only occurring for this one user.

The culprit in this case was:
include_granted_scopes: true,
The other users for whom the consent screen was shown had at some time in the past been given the correct scope, and granted permission. Therefore, on any subsequent grant, the consent screen showed the previously granted scopes. The source code, at some point, changed and removed those particular scopes (and thus, we were only left with the email scope as shown in the question.
Interestingly, the email scope does not have to be consented to, and is automatic so it returned the user to our app with that granted scope, having never showed the consent screen to that one user.
Changing the scope to actually include the correct scopes now allows all users to see the consent screen and grant the proper scopes:
const url = oauth2Client.generateAuthUrl({
access_type: 'offline', // we want refresh tokens
scope: ['email',
'https://www.googleapis.com/auth/gmail.readonly',
'https://www.googleapis.com/auth/gmail.labels',
'https://www.googleapis.com/auth/gmail.modify',
],
state: '{...}', //state object
prompt: 'consent',
hd: 'example.com', // restrict to just our domain
include_granted_scopes: true,
});

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.

Google Identity Services using the token model - Is initTokenClient / requestAccessToken returning the correct scopes?

I'm migrating a web app to the new Google Identity Services API and trying to implement granular scope authorization following this guide:
Using the token model : Granular permissions
The guide states:
Any previously accepted grants from prior sessions or requests will also be included in the response
However, this doesn't seem to work as expected - in situations where only one of two requested scopes are previously granted, I either get both scopes returned or no token at all, depending on how the user responds to the prompt.
This app demonstrates the problem - the Settings page shows which permissions have been granted.
On the first visit, Google's pop-up UX lists the requested permissions enabling the user to choose which ones to grant. If they choose only one of the two, the returned token correctly includes the corresponding scope.
On subsequent visits, the pop-up only lists the permission not granted the first time. Continuing will grant both permissions, cancelling will result in no token returned for either scope. Is this correct? I would have expected that having requested both scopes, I would always get at least the granted scope returned.
If the subsequent pop-ups included a permission check-box as is the case initially, the user would be able to continue using the app without granting the second permission. As it is, there is no UX flow available which allows this.
var permissions = {};
function loadPermissions() {
google.accounts.oauth2.initTokenClient({
client_id: "YOUR_GOOGLE_CLIENT_ID",
scope: "https://www.googleapis.com/auth/calendar.readonly https://www.googleapis.com/auth/drive.appdata",
prompt: "",
callback: rcv
}).requestAccessToken();
function rcv(tok) {
if (google.accounts.oauth2.hasGrantedAllScopes(tok, "https://www.googleapis.com/auth/calendar.readonly"))
permissions.view_calendars = true;
if (google.accounts.oauth2.hasGrantedAllScopes(tok, "https://www.googleapis.com/auth/drive.appdata"))
permissions.drive_appdata = true;
}
}

Can the permission to access user's profile picture be removed in Facebook login oauth?

I can request for permissions specifying the scope like this:
facebook:
type: facebook
client_id: ''
client_secret: ''
scope: "email, user_birthday"
and when logging in, I get asked for permissions for name, profile picture, email and birthday.
I would like to remove the permission for profile picture, since I don't need it in my app. From what I read, name and picture are default permissions, so does that mean they cannot be overridden?

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.

AWS Cognito: Restrict users to a one login at a time

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.