The requirement:
I have to develop the following feature: user clicks a button on my site and then a Google's dialog is popping-up asking for permissions on his calendar for my application. If the user accepts that - my server application (let's say NodeJS) should be able to read his events any time (not only for 30 days).
Here is what I've tried:
I was following these tutorials (both NodeJS and Browser) https://developers.google.com/calendar/quickstart/nodejs
NodeJS tutorial gives me permissions on my account (while I need the users to give permissions on their accounts).
Browser tutorial pop-ups the dialog and asks for permissions. If user accepts the request, then it ends up with permission on the calendar only for 1 hour (so even if I can use the token from my server it doesn't solve the problem). To check it just try to print the Promise's result from handleAuthClick by modifying it a bit
gapi.auth2.getAuthInstance().signIn().then(res => {console.log(res)});
Look at the expires_in (or expires_at) in the printed result:
{
"El": "117770076845340691060",
"Zi": {
"token_type": "Bearer",
"access_token": "ya29.Glb...zr8Yx",
"scope": "https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/plus.me https://www.googleapis.com/auth/userinfo.email openid email profile",
"login_hint": "AJDLj...WlQ",
"expires_in": 3600,
"id_token": "eyJhvGciO...sHxw6HcA",
"session_state": {
"extraQueryParams": {
"authuser": "1"
}
},
"first_issued_at": 1528384733411,
"expires_at": 1528388333411,
"idpId": "google"
},
"w3": {
"Eea": "117770076845340691060",
"ig": "Bryan Gray",
"ofa": "Bryan",
"wea": "Gray",
"Paa": "https://lh3.googleusercontent.com/-zQ8KN1XZtJI/AAAAAAAAAAI/AAAAAAAAAAA/AB...e/s96-c/photo.jpg",
"U3": "alex#khealth.ai"
}
}
Access tokens are only usable for an hour you need a refresh token which will allow you to request a new access token when ever you need one. To do that you need to request offline access.
gapi.signin2.render('glogin', {
'class': 'g-signin',
redirect_uri: 'postmessage',
onsuccess: signInCallback,
cookiepolicy: 'single_host_origin',
accesstype: 'offline',
theme: 'dark'
});
Code from this sample Google oauth2
Related
I'm trying to use the Microsoft Graph API to write calendar events within my company.
First of all let me give you a little bit of context.
I'm building a node API that uses Microsoft Graph to write calendar events, so I configured my application inside the Azure Active Directory with the following application permission
I granted administrator consent as you can see from the picture.
I was also able to get the access token using msal-node
const graphToken = async () => {
const azureConfig = {
auth: {
clientId: process.env.CLIENT_ID,
authority: `https://login.microsoftonline.com/${process.env.TENANT_ID}`,
clientSecret: process.env.CLIENT_SECRET,
},
}
const tokenRequest = {
scopes: [process.env.GRAPH_ENDPOINT + '/.default'],
}
const cca = new msal.ConfidentialClientApplication(azureConfig)
const authRespose = await cca.acquireTokenByClientCredential(tokenRequest)
if (authRespose) {
return authRespose.accessToken
}
return null
}
The only thing that sounds me a little odd, is the scope set to [process.env.GRAPH_ENDPOINT + '/.default'] I tried to change it ex. [process.env.GRAPH_ENDPOINT + '/Calendar.ReadWrite'] but it fires an excepion.
The next thing I'm able to do is retrive all calendars a user have right to write to, using the following Graph endpoint:
https://graph.microsoft.com/v1.0/users/user#example.com/calendars
Now the issue, when I try to do a POST request to write a calendar event for example
POST https://graph.microsoft.com/v1.0/users/{userId}/calendars/{calendarId}/events
{
"subject": "Test",
"body": {
"contentType": "HTML",
"content": "Test"
},
"start": {
"dateTime": "2022-11-09T16:00:00",
"timeZone": "Europe/Rome"
},
"end": {
"dateTime": "2022-11-09T17:00:00",
"timeZone": "Europe/Rome"
}
}
Note that calendarId is one of the id's from the previous call
(Not the default calendar of userId)
I got a 403 Forbidden with the following response
{
"error": {
"code": "ErrorAccessDenied",
"message": "Access is denied. Check credentials and try again."
}
}
I also decoded my token to see if I get some info on the root cause of the 403 error, I found this:
...
"roles": [
"Calendars.Read",
"User.Read.All",
"Calendars.ReadWrite"
],
...
It seems correct to me.
I don't get if it is a scope issue, an authentication issue or something I'm missing, can someone pinpoint me in the right direction?
Thanks in advance
Basically it was my fault.
I messed up with calendar permissions and my test user had a reviewer permission instead of an author one on the calendar I had to write to
once I was able to identify this issue and change the permission the call response was what expected.
I leave this answer as a reference for anyone that encounter this issue
Thanks anyway
I have a React SPA that communicates with the backend API (Azure Function App). I've created an app registration for both the SPA and the Azure Function App following the steps outlined here. Both app registrations are hosted in a separate directory from the Azure Function app since I'm using AD B2C. I'm able to successfully authenticate the user and make requests to the backend. I'm using PKCE as the auth protocol and MSAL.js to manage the authentication flow.
I've configured a standard signup/signin policy for which I'm using Local Account as the Identity provider and username as the user id.
Here's what the login screen looks like:
Here's the relevant code from the SPA which handles auth:
const { instance, accounts, inProgress,} = useMsal();
if (accounts.length > 0) {
msalInstance
.acquireTokenSilent({
account: accounts[0],
scopes: [
"https://APP_URI/user_impersonation",
],
})
.then((token) => {
console.log("token res is", token);
console.log("access token is", token);
})
.catch((err) => {
console.log("err is", err);
});
Here's the return value from calling acquireTokenSilent:
{
"authority":"https://APP_NAMEb2c.b2clogin.com/APP_NAMEb2c.onmicrosoft.com/b2c_1_flow/",
"uniqueId":"581776f4-6e16-454a-a6ae-ecb49f7f04aa",
"tenantId":"",
"scopes":[
"https://APP_NAMEb2c.onmicrosoft.com/reg_api/user_impersonation"
],
"account":{
"homeAccountId":"581776f4-6e16-454a-a6ae-ecb49f7f04aa-b2c_1_flow.07232d62-7285-4737-97eb-87f0f9b7c38e",
"environment":"APP_NAMEb2c.b2clogin.com",
"tenantId":"",
"username":"testUser#gmail.com",
"localAccountId":"581776f4-6e16-454a-a6ae-ecb49f7f04aa",
"name":"unknown",
"idTokenClaims":{
"exp":1663191498,
"nbf":1663187898,
"ver":"1.0",
"iss":"https://APP_NAMEb2c.b2clogin.com/07232d62-7285-4737-97eb-87f0f9b7c38e/v2.0/",
"sub":"581776f4-6e16-454a-a6ae-ecb49f7f04aa",
"aud":"473fe4d9-260b-46ad-9ad1-f4c4a4f211e6",
"nonce":"65c7ec69-2837-4bdf-b9e3-ae38dbb19c48",
"iat":1663187898,
"auth_time":1663187896,
"name":"unknown",
"emails":[
"testUser#gmail.com"
],
"tfp":"B2C_1_flow",
"at_hash":"qOHPceVj3fEhGGlRq6xh4g"
}
},
"idToken":"TD_TOKEN",
"idTokenClaims":{
"exp":1663191498,
"nbf":1663187898,
"ver":"1.0",
"iss":"https://APP_NAMEb2c.b2clogin.com/07232d62-7285-4737-97eb-87f0f9b7c38e/v2.0/",
"sub":"581776f4-6e16-454a-a6ae-ecb49f7f04aa",
"aud":"473fe4d9-260b-46ad-9ad1-f4c4a4f211e6",
"nonce":"65c7ec69-2837-4bdf-b9e3-ae38dbb19c48",
"iat":1663187898,
"auth_time":1663187896,
"name":"unknown",
"emails":[
"testUser#gmail.com"
],
"tfp":"B2C_1_flow",
"at_hash":"qOHPceVj3fEhGGlRq6xh4g"
},
"accessToken":"ACCESS_TOKEN",
"fromCache":true,
"expiresOn":"2022-09-14T21:38:18.000Z",
"correlationId":"9c71acbb-7ed4-4beb-a282-71ec7d924bd8",
"extExpiresOn":"2022-09-14T21:38:18.000Z",
"familyId":"",
"tokenType":"Bearer",
"state":"",
"cloudGraphHostName":"",
"msGraphHost":"",
"fromNativeBroker":false
}
As you can see, the username property has the emailAddress as it's value and not the actual username.
I've not been able to find concrete guidance on how to get the username. The one resource I found said that UserPrincipleName(UPN) is an optional claim and to add this value in the authToken I should add UPN as an optional claim in the token configuration tab, which is not available in B2C AD. I would love to get some guidance on what I'm doing wrong as getting the username should not be this hard, right ?
Edit 1: I can confirm that the username has been set; in the image below the username is denoted by User Principle Name:
The username property in MSAL's AccountInfo object is populated by the email claim in the ID token. The email claim will be an array, and if there are multiple emails, MSAL will only use the first one as username.
To receive UserPrincipleName (UPN) in the ID token, you'll need to set the user attributes in your B2C tenant. Unfortunately this doesn't seem to be possible with standard user-flows, so you'll need to build a custom policy and sign-in with that instead. See for more: User profile attributes
I am making a website like flippa to sell established websites, I want to verify website traffic like flippa do, I searched for how to get reports of visits last 12 months but I can't, what I do make client click on link
https://accounts.google.com/o/oauth2/auth?access_type=offline&client_id=814867900881-28dlacj0v68nh9suspfjlnvjgscalaql.apps.googleusercontent.com&prompt=consent&redirect_uri=http://localhost:3000/redirect&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fanalytics.readonly&state=9612
to get code then use that code to get access_token
POST https://accounts.google.com/o/oauth2/token
code=4/gXCN77EWLDCO_fake_p2tvfakezOg6Mn0fakej2vA.giyP3fakejxeAeYFZr95uygvU3j0dumQI&
client_id=104608secret-secret-secret-secret.apps.googleusercontent.com&
client_secret=90V0FAKE_WkFAKExrHCZti&
redirect_uri=http://www.mywebapp.com/oauth2callback&
grant_type=authorization_code
then I get access token
{
"access_token": "ya29.Il-yBwla5jnTECDlX5rbVk_Oq4hireOUmSzeaTWW2BgYYsBCoPn6pKnQVxIUQtGc0BXblEq_2ZQ-zhrrGeL9xYYuSyd8lvRAoPBUVKUN8liBwx-w8ok2oGh2Cknql2ucew",
"expires_in": 3600,
"refresh_token": "1//031hXU_jNeKEECgYIARAAGAMSNwF-L9Irds9iUaPkcGDDOSHP2-8-1FKYBRW1GaBz1fLzEoDnEcNIm89k-bZzYWXAqie5Or-Fg94",
"scope": "https://www.googleapis.com/auth/analytics.readonly",
"token_type": "Bearer"
}
and don't know what I do next and if what I do is the right way
I need the correct way to get user GA reporting
What you have done so far covers your authentication flow. Now you will need to make a call to the Analytics Reporting API and supply the access token you have created.
Here is a reference for setting up your request:
https://developers.google.com/analytics/devguides/reporting/core/v4/rest/v4/reports/batchGet
There are also samples available for how to request data.
Depending on the system you have, this may help. https://developers.google.com/analytics/devguides/reporting/core/v4
In essence, you add the access token in the google client then perform the request.
For node.js, I found this site that does the particular in node.js although this uses JWT. https://flaviocopes.com/google-analytics-api-nodejs/
const { google } = require('googleapis')
const scopes = 'https://www.googleapis.com/auth/analytics.readonly'
const jwt = new google.auth.JWT(process.env.CLIENT_EMAIL, null, process.env.PRIVATE_KEY, scopes)
const view_id = 'XXXXX'
async function getData() {
const response = await jwt.authorize()
const result = await google.analytics('v3').data.ga.get({
'auth': jwt,
'ids': 'ga:' + view_id,
'start-date': '30daysAgo',
'end-date': 'today',
'metrics': 'ga:pageviews'
})
console.dir(result)
}
getData()
Also, google APIs for Node.js for reference: https://www.npmjs.com/package/googleapis
It contains how to set the access token to set to the oauth2Client and use these in the APIs.
Hope this helps :)
SSO fails "ServerError: AADSTS50058: A silent sign-in request was sent but none of the currently signed in user(s) match the requested login hint"
when I use same account for both work and personal azure account.
I have 2 AAD accounts (one is with my work account and the other one is personal account but both attached with same email and both are using same credentials). When I use msal.js library for single sign on application. It takes me to my work account where it asks me to validate the credentials (using standard pop up dialog) by giving full email address and does not authenticate properly even if give right credentials. As I need to login using my personal account
I expect this should validate using my ad alias#company.com credentials. I tried with different account option in the dialog, but it fails and shows up same full email account.
How can I use my adalias#company.com as a default user id?
Here are the piece of the code I am trying to use.
var msalConfig = {
auth: {
clientId: 'xxxxxxxxxx', // This is your client ID
authority: "https://login.microsoftonline.com/{tenantid}" // This is tenant info
},
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: true
}
};
var graphConfig = {
graphMeEndpoint: "https://graph.microsoft.com/v1.0/me"
};
var requestObj = {scopes: ["user.read", "email"]};
// Is there a way to change here to get the required user id?
var myMSALObj = new Msal.UserAgentApplication(msalConfig);
// Register Callbacks for redirect flow
myMSALObj.handleRedirectCallbacks(acquireTokenRedirectCallBack,
acquireTokenErrorRedirectCallBack);
myMSALObj.handleRedirectCallback(authRedirectCallBack);
function signIn() {
myMSALObj.loginRedirect(requestObj).then(function (loginResponse) {
// Successful login
acquireTokenPopupAndCallMSGraph();
}).catch(function (error) {
// Please check the console for errors
console.log(error);
});
}
Here is the error message I get:
ServerError: AADSTS50058: A silent sign-in request was sent but none of the
currently signed in user(s) match the requested login hint
The expected result is seamless login to other application.
If you want to provide a login_hint to indicate the user you are trying to authenticate try:
var requestObj = {scopes: ["user.read", "email"], loginHint: "adalias#company.com"};
Reference https://github.com/AzureAD/microsoft-authentication-library-for-js/wiki/FAQs#q10-how-to-pass-custom-state-parameter-value-in-msaljs-authentication-request-for-example-when-you-want-to-pass-the-page-the-user-is-on-or-custom-info-to-your-redirect-uri
I am using Amazon Cognito to login users and save a RefreshToken so they don't have to type their password after the initial setup. I need to be able to login with the RefreshToken and get a new RefreshToken to save for next time. However, when I call InitiateAuthAsync, it does not return the RefreshToken.
C#:
var refreshReq = new InitiateAuthRequest();
refreshReq.ClientId = _clientId;
refreshReq.AuthFlow = AuthFlowType.REFRESH_TOKEN_AUTH;
refreshReq.AuthParameters.Add("SECRET_HASH",
SecretHash(_clientId, _clientSecret, username));
refreshReq.AuthParameters.Add("REFRESH_TOKEN", refreshToken);
var clientResp = cognitoProvider.InitiateAuthAsync(refreshReq).Result;
Response:
{
"AuthenticationResult": {
"AccessToken": "<accessToken>",
"ExpiresIn": 3600,
"IdToken": "<idToken>",
"TokenType": "Bearer"
},
"ChallengeParameters": {}
}
And this is the response from the login with a working ResponseToken:
{
"AuthenticationResult": {
"AccessToken": "<accessToken>",
"ExpiresIn": 3600,
"IdToken": "<idToken>",
"RefreshToken": "<refreshToken>",
"TokenType": "Bearer"
},
"ChallengeParameters": {}
}
Apparently this is a bug in the AWS Cognito API. The docs say that InitiateAuth should return an updated RefreshToken, but it does not.
The refresh token is only returned in the initial sign in and it can be used more than once to get a new access token and id token. Be aware of your user pool configuration because it will not work if you are using device tracking and the device use to sign in is not confirmed.
The refresh token is a long-lived token and there's no point returning it as it's still valid for many days. If the default 30-day expiry time is not long enough you can increase it to up to 3650 days.
I agree with the main question here there is an issues if one can't refresh the refresh token.
The best security practice is to regenerate a new Access Token and a new Refresh Token every X minutes. This way if a malicious 3rd party player get a hold on the Access Token / Refresh Token - they will be valid until the next cycle of refreshing the token by the application.
This is very weird that AWS are not supporting such mechanism that is commonly used in the web - such as Passport ID and authentication etc.