I'm struggling a bit with Microsoft authentication architecture to access resources using the Graph API.
Let me explain my use-case: I have an Outlook account, which I need to insert events into the Calendar. I also have a REST API, in Node.js, that should read these events, using /me/events or /users/{id}/events Graph endpoint.
Since it is only one user, I don't need to implement login, but rather have the REST API be able to get an Authorization token to access these resources.
I tried to use the ConfidentialClientApplication class to login using the client_id and client_secret for my application (configured through Azure), but whenever I call the Microsoft Graph after login, I receive a 401.
Assuming that the problem is that the login I'm performing is with an admin account, I added the Application type Calendars.Read permission, to no help.
What am I doing wrong?
I just need to access this users' Calendar :(
Thanks for making it this far!
If you want to use ConfidentialClientApplication to get an access token and call Microsoft Graph API to read users' Calendar, try the code below :
const msal = require('#azure/msal-node');
const fetch = require('node-fetch');
const tenant= '<your tenant ID/name>'
const appID= '<azure ad app id>'
const appSec = '<azure ad app sec>'
const userID = '<user ID/UPN>'
const config = {
auth: {
clientId: appID,
authority: "https://login.microsoftonline.com/" + tenant,
clientSecret: appSec
}
}
function readUserCalendar(userID,accessToken){
const URL = 'https://graph.microsoft.com/v1.0/users/'+userID+'/events?$select=subject'
fetch(URL,{headers: { 'Authorization': 'Bearer ' + accessToken}})
.then(res => res.json())
.then(json => console.log(json));
}
const pca = new msal.ConfidentialClientApplication(config);
const acquireAccessToken = pca.acquireTokenByClientCredential({
scopes: ["https://graph.microsoft.com/.default"]
});
acquireAccessToken.then(result=>{readUserCalendar(userID,result.accessToken)})
Permissions grand to azure ad app in this case :
My test account calendar data:
Result :
Related
Desired Behaviour
I am setting up a Node.js web application using Azure AD B2C authentication and authorisation.
It is a confidential, server side, client (i.e. - not a Single Page Application).
The desired behaviour is:
Authenticate and authorise a user via login using Azure ADB2C
Based on a successful login, allow users to call routes in the Node web app
My specific question is at the bottom of this post, but is essentially:
Given that, if an accessToken is present in req.session, my
application will return the result from calling ANY endpoint I choose
(even if it is not related to any of the 'scopes' defined when
'exposing the api'), are the scopes that are defined when 'exposing an
API' essentially 'for information purposes only' - both for
application admins and end users? Or should they somehow be enforced in each relevant Express route handler?
Research
I have done extensive reading and video watching around the topic, including:
GitHub Repositories:
active-directory-b2c-msal-node-sign-in-sign-out-webapp
active-directory-b2c-javascript-nodejs-webapi
learn.microsoft.com articles:
https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-overview
https://learn.microsoft.com/en-au/azure/active-directory-b2c/configure-a-sample-node-web-app
https://docs.microsoft.com/azure/active-directory-b2c/enable-authentication-in-node-web-app
https://docs.microsoft.com/azure/active-directory-b2c/configure-authentication-in-sample-node-web-app-with-api
https://docs.microsoft.com/azure/active-directory-b2c/enable-authentication-in-node-web-app-with-api
Videos:
Identity for Developers Playlist by Microsoft Security
OAuth 2.0 and OpenID Connect (in plain English) by OktaDev
What I've Tried
I have been able to implement a basic prototype consisting of:
Node.js web application (a confidential, server side, client, not a Single Page App)
Sign Up, Sign In and Edit Profile User flows
2 x Application Registrations in Azure - one for the Web App and one for the Web App API
I have 'exposed the API' in the Web App API registration, eg: :
https://my-tenant-name.onmicrosoft.com/my-api-uri-thing/tasks.read
https://my-tenant-name.onmicrosoft.com/my-api-uri-thing/tasks.write
I have 'added permissions' to the Web App registration (to access the Web App API registration), eg:
MY-APP-API (2)
tasks.read, Delegated, Admin Consent Required - Yes, Granted for MY-APP
tasks.write, Delegated, Admin Consent Required - Yes, Granted for MY-APP
Microsoft Graph (2)
offline_access, Delegated, Admin Consent Required - No, Granted for MY-APP
openid, Delegated, Admin Consent Required - No, Granted for MY-APP
So the workflow is as follows (based on this code):
User clicks on a '/signin' link
The relevant Express route handler passes through the required scopes to getAuthCode():
app.get('/signin',(req, res)=>{
//Initiate a Auth Code Flow >> for sign in
//Pass the api scopes as well so that you received both the IdToken and accessToken
getAuthCode(process.env.SIGN_UP_SIGN_IN_POLICY_AUTHORITY,apiConfig.webApiScopes, APP_STATES.LOGIN, res);
});
Source
The value of the scopes parameter is:
['https://${process.env.TENANT_NAME}.onmicrosoft.com/my-api-uri-thing/tasks.read','https://${process.env.TENANT_NAME}.onmicrosoft.com/my-api-uri-thing/tasks.write']
Upon successful sign in, the user is redirected to /redirect
The relevant Express route handler passes through a tokenRequest object to get an accessToken which is then added to req.session:
app.get('/redirect',(req, res)=>{
if (req.query.state === APP_STATES.LOGIN) {
// prepare the request for calling the web API
tokenRequest.authority = process.env.SIGN_UP_SIGN_IN_POLICY_AUTHORITY;
tokenRequest.scopes = apiConfig.webApiScopes;
tokenRequest.code = req.query.code;
confidentialClientApplication.acquireTokenByCode(tokenRequest)
.then((response) => {
req.session.accessToken = response.accessToken;
req.session.givenName = response.idTokenClaims.given_name;
console.log('\nAccessToken:' + req.session.accessToken);
res.render('signin', {showSignInButton: false, givenName: response.idTokenClaims.given_name});
}).catch((error) => {
console.log(error);
res.status(500).send(error);
});
}else{
res.status(500).send('We do not recognize this response!');
}
});
User calls a 'protected API'
The relevant Express route handler checks if req.session contains an accessToken value
If the accessToken is present, it makes an http request to the desired endpoint using axios and passes through the accessToken as the Bearer token in the headers of the request.
The 'protected content' is then returned
app.get('/api', async (req, res) => {
if(!req.session.accessToken){
//User is not logged in and so they can only call the anonymous API
try {
const response = await axios.get(apiConfig.anonymousUri);
console.log('API response' + response.data);
res.render('api',{data: JSON.stringify(response.data), showSignInButton: true, bg_color:'warning'});
} catch (error) {
console.error(error);
res.status(500).send(error);
}
}else{
//Users have the accessToken because they signed in and the accessToken is still in the session
console.log('\nAccessToken:' + req.session.accessToken);
let accessToken = req.session.accessToken;
const options = {
headers: {
//accessToken used as bearer token to call a protected API
Authorization: `Bearer ${accessToken}`
}
};
try {
const response = await axios.get(apiConfig.protectedUri, options);
console.log('API response' + response.data);
res.render('api',{data: JSON.stringify(response.data), showSignInButton: false, bg_color:'success', givenName: req.session.givenName});
} catch (error) {
console.error(error);
res.status(500).send(error);
}
}
});
Question
Given that, if an accessToken is present in req.session, my application will return the result from calling ANY endpoint I choose (even if it is not related to any of the 'scopes' defined when 'exposing the api'), are the scopes that are defined when 'exposing an API' essentially 'for information purposes only' - both for application admins and end users?
Or should I be coding in a conditional statement in each relevant Express route handler that says:
IF the required scope for accessing this content is present in this access token,
THEN you can access this content
and therefore 'enforces' the scope that has been defined and consented to by the user.
With my current level of understanding, I am assuming that these scopes ARE just for information purposes only, because I haven't seen any examples where the scopes are enforced through code.
I am facing issue while trying to generated token for One-Drive access. As I have requirement where user can get all the files from there One Drive using my application.
I tried below code but I am getting error.
{"error":"invalid_grant","error_description":"AADSTS65001: The user or administrator has not consented to use the application with ID. Send an interactive authorization request for this user and resource.\r\nTrace ID: 33a0dd6a-6984-4c0a-8f74-6fbcd9c54301\r\nCorrelation ID: 265ca054-ab98-450c-8281-851ef6b0fdc3\r\nTimestamp: 2022-11-24 15:56:04Z","error_codes":[65001],"timestamp":"2022-11-24 15:56:04Z","trace_id":"33a0dd6a-6984-4c0a-8f74-6fbcd9c54301","correlation_id":"265ca054-ab98-450c-8281-851ef6b0fdc3","suberror":"consent_required"}
Find my code that I am trying.
public async Task GetTokenAsync(string tenant, string clientId, string
clientSecret, string username, string password)
{
HttpResponseMessage resp;
string token;
using (var httpClient = new HttpClient())
{
httpClient.DefaultRequestHeaders.Accept.Add(
new ("application/x-www-form-
urlencoded"));
var req = new HttpRequestMessage(HttpMethod.Post, $"https://login.microsoftonline.com/{tenant}/oauth2/token/");
req.Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
{"grant_type", "password"},
{"client_id", clientId},
{"client_secret", clientSecret},
{"resource", "https://graph.microsoft.com/"},
{"username", username},
{"password", password}
});
resp = await httpClient.SendAsync(req);
string content = await resp.Content.ReadAsStringAsync();
var jsonObj = System.Text.Json.JsonSerializer.Deserialize<dynamic>(content);
token = jsonObj["access_token"];
}
return token;
}
Nothing
I tried to reproduce the same in my environment and got the same error as below:
The error usually occurs if you have not consented Admin Consent to the API Permissions you have granted like below:
To resolve the error, I created the Azure AD Application and granted Admin consent to the API Permissions:
To generate the access token, I used to the below parameters:
GET https://login.microsoftonline.com/TenantId/oauth2/token
grant_type:password
client_id:53f9d6e0-f9e8-4620-9e45-XXXXX
client_secret:Msd8Q~q~-2gb4sooQVGDIQQAI92gXXXXX
resource:https://graph.microsoft.com/
username:username
password:Password
I am able to generate the access token successfully as below:
If still the issue persists, try updating the below settings in Azure Enterprise Application like below:
Go to Azure Active Directory -> Enterprise applications -> User settings -> Consent and permissions
Otherwise, you can Grant admin consent by using below Endpoint, Login as Admin and Accept:
https://login.microsoftonline.com/TenantId/adminconsent?client_id=ClientID
I have created an Azure B2C custom attribute called IsAdmin on the portal, added it to a Sign In / Sign Up user flow, and then using the Graph API, successfully created a new user with IsAdmin = true. If I then sign in using that new user I can see IsAdmin returned in the token as a claim. So far so good.
However I can't seem to see that custom attribute when querying via Graph API, nor can I search for it.
var user = await graphClient.Users["{GUID HERE}"]
.Request()
.GetResponseAsync();
The user is returned, but the custom attribute is not present.
var results = await graphClient.Users
.Request()
.Filter("extension_anotherguid_IsAdmin eq true")
.GetAsync();
Returns no results.
Does anyone have any idea?
When storing custom attribute in a B2C tenant, a microsoft's managed app registration is created :
Take the app id of this app registration, remove the dashes in the id and then use it like below :
import requests
# if your app registration b2c extensions app id id is aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee :
b2c-extensions-app-id-without-dashes="aaaaaaaabbbbccccddddeeeeeeeeeeee"
url = f"https://graph.microsoft.com/v1.0/users/?$select=extension_{b2c-extensions-app-id-without-dashes}_IsAdmin"
headers = {
'Content-type': 'application/json',
'Authorization': 'Bearer ' + msgraph_token
}
r = requests.request("GET", url, headers=headers)
Extensions are not returned by default. You need specify the extension in Select
var user = await graphClient.Users["{GUID HERE}"]
.Request()
.Select("extension_anotherguid_IsAdmin")
.GetResponseAsync();
The value should be available through AdditionalData.
var extValue = user.AdditionalData["extension_anotherguid_IsAdmin"];
Resources:
Extensibility
I need to access the calendars of Azure AD users. I do it with the code below:
var scopes = new[] { "https://graph.microsoft.com/.default" };
var tenantId = "MY_TENANT_ID";
var clientId = "APP_ID";
var clientSecret = "APP_SECRET";
var options = new TokenCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
};
var clientSecretCredential = new ClientSecretCredential(
tenantId, clientId, clientSecret, options);
var graphClient = new GraphServiceClient(clientSecretCredential, scopes);
var user = await graphClient.Users["USER_ID"]
.Calendar.Events
.Request()
.GetAsync();
As a result, I get the following error:
Status Code: Unauthorized
Microsoft.Graph.ServiceException: Code: NoPermissionsInAccessToken
Message: The token contains no permissions, or permissions can not be understood.
In Azure AD->App Registrations->API permissions I have got Calendars.Read permission, but don't have User.Read.All permission. So the question is can I get data from calendars without User.Read.All permission and if I can what am I doing wrong?
According to your code snippet, you used client credential flow to obtain the authentication and call graph api to list all calendar events for a specific user.
So you need to make your azure ad application to get Calendar.Read application permission. Without the permission then you can't call the api successfully
How to add application permission:
In my web project i want to enable the user to login with username / password and Microsoft Account.
Tech - Stack:
Asp.Net Core WebApi
Angular
Azure App Service
First i created the username / password login. Like this:
StartUp.cs:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
{
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(Configuration["JWTKey"].ToString())),
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true
};
});
Login Method:
public async Task<IActionResult> ClassicAuth(AuthRequest authRequest)
{
tbl_Person person = await _standardRepository.Login(authRequest.Username, authRequest.Password);
if (person != null)
{
var claims = new[]
{
new Claim(ClaimTypes.GivenName, person.PER_T_Firstname),
};
var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(_config["JWTKey"].ToString()));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.Now.AddHours(24),
SigningCredentials = creds
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return Ok(tokenHandler.WriteToken(token));
}
else
return Unauthorized("Invalid login data");
}
And secured my api enpoints with [Authorize].So far so good...that works.
Now i want to add a login method with Microsoft Account. I use Azure App Service Authentication / Authorization for that (https://learn.microsoft.com/de-de/azure/app-service/overview-authentication-authorization).
I configured the auth provider and i'm able to start the auth flow with a custom link in my angular app:
Login with Microsoft - Account
This works and i can retrieve the access token from my angular app with this:
this.httpClient.get("https://mysite.azurewebsites.net/.auth/me").subscribe(res => {
console.log(res[0].access_token);
});
Now the problem:
access_token seems not a valid JWT Token. If i copy the token and go to https://jwt.io/ it is invalid.
When i pass the token to my API i get a 401 - Response. With seems logical because my API checks if the JWT Token is signed with my custom JWT Key and not the Key from Microsoft.
How can I make both login methods work together? I may have some basic understanding problems at the moment.
It seems you want your Angular app calling an ASP.NET Core Web API secured with Azure Active Directory, here is a sample works well for that.
The most important step is register the app in AAD.
By the way, if you want to enable users to login one project with multiple ways in azure, you can use multiple sign-in providers.