MSAL and Azure AD: What scopes should I pass when I just want to get the user ID? - oauth-2.0

I'm using MSAL to get an ID Token which is then used to access an Web API app. I've got a couple of questions and I was wondering if someone could help me understand what's going on.
Let me start with the authentication process in the client side. In this case, I'm building a Windows Forms app that is using the following code in order to authenticate the current user (ie, in order to get an ID Token which will be used to validate the user when he tries to access a Web API app):
//constructor code
_clientApp = new PublicClientApplication(ClientId,
Authority, //which url here?
TokenCacheHelper.GetUserCache());
_scopes = new []{ "user.read" }; //what to put here?
//inside a helper method
try {
return await _clientApp.AcquireTokenSilentAsync(_scopes, _clientApp.Users.FirstOrDefault());
}
catch (MsalUiRequiredException ex) {
try {
return await _clientApp.AcquireTokenAsync(_scopes);
}
catch (MsalException ex) {
return null;
}
}
The first thing I'd like to clear is the value that should be used for the authority parameter. In this case, I'm using an URL on the form:
https://login.microsoftonline.com/{Tenant}/oauth2/v2.0/token
However, I'm under the impression that I could also get away with something like this:
https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration
It seems like one endpoint is specific to my Azure AD while the other looks like a general (catch all) URL...Where can I find more information about these endpoints and on what's the purpose of each...
Another thing that I couldn't quite grasp is the scope. I'm not interested in querying MS Graph (or any other Azure related service for that matter). In previous versions of the MSAL library, it was possible to reference one of the default scopes. However, it seems like that is no longer possible (at least, I tried and got an exception saying that I shouldn't pass the default scopes...).
Passing an empty collection (ex.: new List<string>()) or null will also result in an error. So, in this case, I've ended passing the user.read scope (which, if I'm not mistaken, is used by MS Graph API. This is clearly not necessary, but was the only way I've managed to get the authentication process working. Any clues on how to perform the call when you just need to get an ID Token? Should I be calling a different method?
Moving to the server side, I've got a Web API app whose access is limited to calls that pass an ID token in the authentication header (bearer). According to this sample, I should use something like this:
private void ConfigureAuth(IAppBuilder app) {
var authority = "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration";
app.UseOAuthBearerAuthentication(
new OAuthBearerAuthenticationOptions {
AccessTokenFormat = new JwtFormat(GetTokenValidationParameters(),
new OpenIdConnectCachingSecurityTokenProvider(authority)),
Provider = new OAuthBearerAuthenticationProvider {
OnValidateIdentity = ValidateIdentity
}
});
}
Now, this does work and it will return 401 for all requests which don't have a valid ID Token. There is one question though: is there a way to specify the claim from the Ticket's Identity that should be used for identifying the username (User.Identity.Name of the controller)? In this case, I've ended handling the OnValidateIdentity in order to do that with code that looks like this:
private Task ValidateIdentity(OAuthValidateIdentityContext arg) {
//username not getting correctly filled
//so, i'm handling this event in order to set it up
//from the preferred_username claim
if (!arg.HasError && arg.IsValidated) {
var identity = arg.Ticket.Identity;
var username = identity.Claims.FirstOrDefault(c => c.Type == "preferred_username")?.Value ?? "";
if (!string.IsNullOrEmpty(username)) {
identity.AddClaim(new Claim(ClaimTypes.Name, username));
}
}
return Task.CompletedTask;
}
As you can see, I'm searching for the preferred_username claim from the ID Token (which was obtained by the client) and using its value to setup the Name claim. Is there any option that would let me do this automatically? Am I missing something in the configuration of the OAuthBearerAuthenticationMiddleware?

Regarding your First Query -
Where can I find more information about these endpoints and on what's the purpose of each...
Answer -
https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration
The {tenant} can take one of four values:
common -
Users with both a personal Microsoft account and a work or school account from Azure Active Directory (Azure AD) can sign in to the application.
organizations -
Only users with work or school accounts from Azure AD can sign in to the application.
consumers -
Only users with a personal Microsoft account can sign in to the application.
8eaef023-2b34-4da1-9baa-8bc8c9d6a490 or contoso.onmicrosoft.com -
Only users with a work or school account from a specific Azure AD tenant can sign in to the application. Either the friendly domain name of the Azure AD tenant or the tenant's GUID identifier can be used.
Regarding your Second Query on Scope -
Answer - Refer to this document - OpenID Connect scopes
Regarding your Third Query on Claim -
Answer - Refer to this GIT Hub sample - active-directory-dotnet-webapp-roleclaims

Related

ASP.NET Core Identity x Docker - Confirmation link invalid on other instances

I am currently developing a web API with ASP.NET Core, using Microsoft Identity Core as for the identity management. When a user registers, it is sent an email with a confirmation link - pretty basic so far.
The problem comes when publishing my API to Azure using a containerized Azure App Service, and when setting the number of instances to 2 or more. The confirmation link seems to be working only half the time; tests on my dev machine with multiple Docker containers running seemed to confirm that fact, as the confirmation link could be validated only on the instance the user had registered on (hence the instance where the confirmation link was created).
Having dug a bit on the subject by reading this article by Steve Gordon, and explored the public GitHub code for Identity Core, I still don't understand why different container instances would return different results when validating the token, as the validation should mainly be based on the user SecurityStamp (that remains unchanged between the instances becauses they all link to the same database).
Also, enabling 'debug' logging for the Microsoft.AspNetCore.Identity only logged
ValidateAsync failed: unhandled exception was thrown.
during token validation from the DataProtectorTokenProvider.ValidateAsync() method from AspNetCore.Identity, so it is not very helpful as I can't see precisely where the error happens...
May this be linked to the token DataProtector not being the same on different instances? Am I searching in the wrong direction? Any guess, solution or track for this?
Help would be immensely appreciated 🙏
Here is some simplified code context from my app for the record.
UserManager<User> _manager; // Set from DI
// ...
// Creating the user and sending the email confirmation link
[HttpGet(ApiRoutes.Users.Create)]
public async Task<IActionResult> RegisterUser(UserForRegistrationDto userDto)
{
var user = userDto.ToUser();
await _manager.CreateAsync(user, userDto.Password);
// Create the confirmation token
var token = await _manager.CreateEmailConfirmationTokenAsync(user);
// Generate the confirmation link pointing to the below 'ConfirmEmail' endpoint
var confirmationLink = Url.Action("ConfirmEmail", "Users",
new { user.Email, token }, Request.Scheme);
await SendConfirmationEmailAsync(user, confirmationLink); // Some email logic elsewhere
return Ok();
}
// Confirms the email using the passed token
[HttpGet(ApiRoutes.Users.ValidateEmail)]
public async Task<IActionResult> ConfirmEmail(string email, string token)
{
var user = await _userManager.FindByEmailAsync(email);
if (user == null)
{
return NotFound();
}
var result = await _userManager.ConfirmEmailAsync(user, token);
if (!result.Succeeded)
{
return BadRequest();
}
return Ok();
}
Token generated based on security stamp but Identity uses DataProtector to protect the token content. By default the data protection keys stored at location %LOCALAPPDATA%\ASP.NET\
If the application runs on single machine it is perfectly fine as there is no scope for key mismatch. But deployed on multiple instances the tokens will not work sometimes as the Keys are different on different machines and there is no guarantee the generation of token and validation of token will come to same instance.
To solve user redis or azurekeyvault
https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview?view=aspnetcore-6.0#persisting-keys-with-redis

Issue securig MultiTenant Application with Identity Server & SaasKit

I've created a multitenant ASP.NET Core Web API and secured by Identity Server. I've used SaasKit Multitenancy nugget for multitenancy. and multi-tenancy is working fine. I'm facing issue with the authentication.
I've used the different hostname for the different tenant. I've defined different scope per tenant and retrieve token based on the scope for the tenant. The first request to the API works fine. but then when the second tenant tries to access the API it errors out with audience validation. Token has the valid audience but the Web API still use the audience of the first request.
Here is the code in my API:
services.AddSingleton<IOptionsMonitor<IdentityServerAuthenticationOptions>, IdentityServerTenantProvider>();
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.RequireHttpsMetadata = false;
});
And here is the implementation of IdentityServerTenantProvider
protected override IdentityServerAuthenticationOptions Create(IdentityServerAuthenticationOptions options, string name, string tenant, string tenantHostName)
{
var currentTenantContext = this._memoryCache.Get(tenantHostName) as TenantContext<PaperSaveAPITenant>;
options.Authority = currentTenantContext.Tenant.Authority;
options.ApiName = currentTenantContext.Tenant.ApiName;
return base.Create(options, name, tenant, tenantHostName);
}
For tenant 2, it is setting proper API Name and Authority but still while validating the token API uses the API name of the first tenant.
I'm able to resolve it using following code.
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme,
jwtOptions =>
{
},
referenceOptions => {
});
And then registering the IOptionMonitor for JwtBearerOptions (for JWT Tokens) & OAuth2IntrospectionOptions (For Reference Tokens). I used both because my API support both type of tokens. (Reference & JWT). If you are using only one of the then you don't need to specify both.
services.AddSingleton<IOptionsMonitor<JwtBearerOptions>, JWTOptionsProvider>();
services.AddSingleton<IOptionsMonitor<OAuth2IntrospectionOptions>, OAuth2IntrospectionOptionsProvider>();
and you need to create to class "JWTOptionsProvider" inherit from TenantOptionsProvider & "OAuth2IntrospectionOptionsProvider" inherit from TenantOptionsProvider

How to implement OAuth2 for a single tool, without using it as my application's authorization solution

I currently have a MVC site, in .NET Core, backed by a public API. My users must log in (there are no [Anonymous] controllers), and authentication is already successfully being done using the DotNetCore.Authentication provider. All that is well and good.
What I'm now trying to do (by user request) is implement functionality for a user to read and view their Outlook 365 calendar on a page within my site. It doesn't seem too hard on the surface... all I have to do is have them authenticate through microsoftonline with my registered app, and then -- once they have given approval -- redirect back to my app to view their calendar events that I am now able to pull (probably using Graph).
In principle that seems really easy and straightforward. My confusion comes from not being able to implement authentication for a single controller, and not for the entire site. All of the OAuth2 (or OpenID, or OWIN, or whatever your flavor) examples I can find online -- of which there are countless dozens -- all want to use the authorization to control the User.Identity for the whole site. I don't want to change my sitewide authentication protocol; I don't want to add anything to Startup.cs; I don't want anything to scope outside of the one single controller.
tldr; Is there a way to just call https://login.microsoftonline.com/common/oauth2/v2.0/authorize (or facebook, or google, or whatever), and get back a code or token that I can use for that user on that area of the site, and not have it take over the authentication that is already in place for the rest of the site?
For anybody else who is looking for this answer, I've figured out (after much trial and error) how to authenticate for a single user just for a short time, without using middleware that authenticates for the entire application.
public async Task<IActionResult> OfficeRedirectMethod()
{
Uri loginRedirectUri = new Uri(Url.Action(nameof(OfficeAuthorize), "MyApp", null, Request.Scheme));
var azureADAuthority = #"https://login.microsoftonline.com/common";
// Generate the parameterized URL for Azure login.
var authContext = GetProviderContext();
Uri authUri = await authContext.GetAuthorizationRequestUrlAsync(_scopes, loginRedirectUri.ToString(), null, null, null, azureADAuthority);
// Redirect the browser to the login page, then come back to the Authorize method below.
return Redirect(authUri.ToString());
}
public async Task<IActionResult> OfficeAuthorize()
{
var code = Request.Query["code"].ToString();
try
{
// Trade the code for a token.
var authContext = GetProviderContext();
var authResult = await authContext.AcquireTokenByAuthorizationCodeAsync(code, _scopes);
// do whatever with the authResult, here
}
catch (Exception ex)
{
System.Diagnostics.Trace.WriteLine(ex.ToString());
}
return View();
}
public ConfidentialClientApplication GetContext()
{
var clientId = "OfficeClientId;
var clientSecret = "OfficeClientSecret";
var loginRedirectUri = new Uri(#"MyRedirectUri");
TokenCache tokenCache = new MSALSessionCache().GetMsalCacheInstance();
return new ConfidentialClientApplication(
clientId,
loginRedirectUri.ToString(),
new ClientCredential(clientSecret),
tokenCache,
null);
}
I don't know if that will ever be helpful to anybody but me; I just know that it's a problem that doesn't seem to be easily solved by a quick search.

To retrieve access token

I have created a MVC application to escalate work to other person inside my organization. I have added all the members in my organization to AAD,
and registered an application there, created app service and linked that app service to registered app with SSO enabled.
Now every time someone visits the app, they can login successfully using their respective credential.
What I want to do know is to retrieve all the members in my AAD and display them inside dropdown list so that anyone can escalate to others by just looking in the dropdown list.
I have tried with sample graph SDK to get the name of users in my organization
with this code
private string redirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
private string appId = ConfigurationManager.AppSettings["ida:AppId"];
private string appSecret = ConfigurationManager.AppSettings["ida:AppSecret"];
private string scopes = ConfigurationManager.AppSettings["ida:GraphScopes"];
public async Task<string> GetUserAccessTokenAsync()
{
string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
HttpContextWrapper httpContext = new HttpContextWrapper(HttpContext.Current);
TokenCache userTokenCache = new SessionTokenCache(signedInUserID, httpContext).GetMsalCacheInstance();
//var cachedItems = tokenCache.ReadItems(appId); // see what's in the cache
ConfidentialClientApplication cca = new ConfidentialClientApplication(
appId,
redirectUri,
new ClientCredential(appSecret),
userTokenCache,
null);
try
{
AuthenticationResult result = await cca.AcquireTokenSilentAsync(scopes.Split(new char[] { ' ' }), cca.Users.First());
return result.AccessToken;
}
// Unable to retrieve the access token silently.
catch (Exception)
{
HttpContext.Current.Request.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties() { RedirectUri = "/" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
throw new ServiceException(
new Error
{
Code = GraphErrorCode.AuthenticationFailure.ToString(),
Message = Resource.Error_AuthChallengeNeeded,
});
}
}
with some change in scope.
<add key="ida:AppId" value="xxxxx-xxxxxxx-xxxxxx-xxxxxxx"/>
<add key="ida:AppSecret" value="xxxxxxxxxxx"/>
<add key="ida:RedirectUri" value="http://localhost:55065/"/>
<add key="ida:GraphScopes" value="User.ReadBasic.All User.Read Mail.Send Files.ReadWrite"/>
This enables me to get basic details of all user in my organization.
But how I can achieve this in my app where authentication related stuffs are done in azure only, and there is no code for authentication and authorization in entire solution.
Thanks
Subham, NATHCORP, INDIA
But how I can achieve this in my app where authentication related stuffs are done in azure only, and there is no code for authentication and authorization in entire solution.
Based on my understanding, you are using the build-in feature App Service Authentication / Authorization. You could follow here to configure your web app to use AAD login. And you need to configure the required permissions for your AD app as follows:
Note: For Azure AD graph, you need to set the relevant permissions for the Windows Azure Active Directory API. For Microsoft Graph, you need to configure the Microsoft Graph API.
Then, you need to configure additional settings for your web app. You could access https://resources.azure.com/, choose your web app and update App Service Auth Configuration as follows:
Note: For using Microsoft Graph API, you need to set the resource to https://graph.microsoft.com. Details, you could follow here.
For retrieving the access token in your application, you could get it from the request header X-MS-TOKEN-AAD-ACCESS-TOKEN. Details, you could follow Working with user identities in your application.
Moreover, you could use Microsoft.Azure.ActiveDirectory.GraphClient package for Microsoft Azure Active Directory Graph API, Microsoft.Graph package for Microsoft Graph API using the related access token.

Need to Authenticate the Users using Twitter, Google, Facebook Apis through oauth plugin?

First i briefly want to know how you can use oauth works. What we need to pass in this plugin and what this plugin will return. Do we have to customize the plugin for different php frameworks. I have seen that their is a different extension of oauth for different framework, why is that?
I need to authenticate the users using social networks in yii framework and I have integrated eouath extension of yii to use oauth and have made an action to use access the ads service of google user like this
public function actionGoogleAds() {
Yii::import('ext.eoauth.*');
$ui = new EOAuthUserIdentity(
array(
//Set the "scope" to the service you want to use
'scope'=>'https://sandbox.google.com/apis/ads/publisher/',
'provider'=>array(
'request'=>'https://www.google.com/accounts/OAuthGetRequestToken',
'authorize'=>'https://www.google.com/accounts/OAuthAuthorizeToken',
'access'=>'https://www.google.com/accounts/OAuthGetAccessToken',
)
)
);
if ($ui->authenticate()) {
$user=Yii::app()->user;
$user->login($ui);
$this->redirect($user->returnUrl);
}
else throw new CHttpException(401, $ui->error);
}
If I want to use other services like linkedin, facebook, twitter just to sign up the user should I just change the scope and parameters or also have to make some changes elsewhere. How do I store user information in my own database?
In simple case you may use the table "identities" with fields "*external_id*" and "provider". Every OAuth provider must give unique user identificator (uqiue only for that provider). To make it unique on your site you may use pair with provider predefined name (constant). And any other additional fields (if a provider gives it).
In the same table you should store identity data of internal authorization, with provider name 'custom' (for ex.). To store password and other data use a separate table, and PK from this table would be your "*external_id*". Universal scheme.
And PHP, something like this:
class UserIdentity extends CUserIdentity
{
protected $extUserID;
public function __construct($extUserID)
{
$this->extUserID = $extUserID;
}
...
public function authenticate()
{
...
//After search $this->extUserID as PK in users table (built-in authorization)
...
$identity = Identities::model()->findByAttributes(array(
'ext_id' => $this->extUserID,
'service' => 'forum',
));
if(!count($identity))
{
$identity = new Identities;
$identity->ext_id = $this->extUserID;
$identity->service = 'forum';
$identity->username = $userData['username'];
$identity->save();
}
$this->setState('id', $identity->id);
...
}
}

Resources