Currently I'm implementing Microsoft Authentication Library(MSAL) on my C# .NET framework webapp (single tenant) and when I acquire the token using the code from Owin I'm getting the wrong GUID for the user/tenant in my confidential app.
This is the return from the confidential app(dc3... is the UserId and cf3.. is the TenantId), this is from a different directory on Azure.
But the claims generated by C# have the correct values:
If I check the object from the confidential app I can see inside "TenantProfiles" the same values as the above (f81 and e24), the correct ones.
But since the Claims have different values as the Confidential App, I cannot get the user with GetAccountAsync(), because it tries to find a user based on "dc3" GUID not "f81" GUID. I can get the user using a filter on GetAccountsAsync(), but this method is deprecated.
Here's my code
public static string appKey = ConfigurationManager.AppSettings["ida:AppKey"];
private static string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"]; //https://login.microsoftonline.com/{0}
public static string tenant = ConfigurationManager.AppSettings["ida:Tenant"];
public static string redirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
public static readonly string Authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant) + "/v2.0";
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions() { CookieSecure = CookieSecureOption.SameAsRequest });
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
Authority = Startup.Authority,
ClientId = Startup.clientId,
RedirectUri = Startup.redirectUri,
PostLogoutRedirectUri = Startup.redirectUri,
Scope = OpenIdConnectScopes.OpenIdProfile,
Notifications = new OpenIdConnectAuthenticationNotifications()
{
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
AuthenticationFailed = OnAuthorizationFailed
}
});
}
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
{
var app = IdentityApiUtility.BuildConfidentialClientApplication();
var result = await app.AcquireTokenByAuthorizationCode(new[] { "https://graph.microsoft.com/.default" }, notification.Code).ExecuteAsync();
}
and
public static IConfidentialClientApplication BuildConfidentialClientApplication()
{
if (clientapp == null)
{
clientapp = ConfidentialClientApplicationBuilder
.Create(Startup.clientId)
.WithClientSecret(Startup.appKey)
.WithRedirectUri(Startup.redirectUri)
.WithAuthority(Startup.Authority)
.Build();
}
return clientapp;
}
/// <summary>
/// Gets an auth code on behalf of the current user
/// </summary>
private AuthenticationResult GetOpenIdConnectAuth()
{
try
{
string userObjectID = $"{ClaimsPrincipal.Current.GetObjectId()}.{ClaimsPrincipal.Current.GetTenantId()}";
var app = BuildConfidentialClientApplication();
var scopes = new[] { "https://graph.microsoft.com/.default" };
//The userObjectId here starts with f81, which I got from the claims. But the user in the ConfidentialApp starts with dc3 which from another Azure Directory
var account = app.GetAccountAsync(userObjectID).Result;
var accessToken = app.AcquireTokenSilent(scopes, account).ExecuteAsync().Result;
return accessToken;
}
catch (Exception ex)
{
throw new Exception("Authentication Error in GetOpenIdConnectAuth method");
}
}
I already checked clientid/secret/tenant multiple times just to be sure that I wasn't sending the wrong authority/tenant and this is not the case. Does anyone have a suggestion how I can get the user from the ConfidentialApp or what I'm doing wrong?
Related
I'm using OWIN combined with Azure Active Directory App Registration as my authentication method on my MVC Web App as below to restrict the login user within a single domain. This part is functioning well.
public partial class Startup
{
private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
private static string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
private static string tenant = ConfigurationManager.AppSettings["ida:Tenant"];
private static string postLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"];
string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = context =>
{
context.HandleResponse();
context.Response.Redirect("/Home/ErrorPage?message=" + context.Exception.Message);
return Task.FromResult(0);
}
}
});
}
}
Now I want to use another Azure Active Directory App Registration to restrict few users to only one admin configuration page. I don`t know if it doable and how to do it.
This is current controller attribute code to redirect user to a username/password login page before accessing admin configuration page. How can I change it to be redirected to an AAD login. In this way, I can configure the qualified user in AAD without maintain any username and password.
public class ConfigLoginAttribute : AuthorizeAttribute
{
public bool Ignore = true;
public ConfigLoginAttribute(bool ignore = true)
{
Ignore = ignore;
}
public override void OnAuthorization(AuthorizationContext filterContext)
{
if (Ignore == false)
{
return;
}
if (CookieHelper.GetCookie("username") == "")
{
filterContext.Result = new RedirectToRouteResult("Default", new RouteValueDictionary { { "Action", "AdminLogin" }, { "Controller", "Config" } });
return;
}
}
}
I`m new to this area and even English. Hopefully I explained it clear.
Thank you guys so much in advance.
I'm having an issue using Azure AAD appRoles and MVC, i have modified the manifest added a few roles and assigned them to a couple of users.
However when i try using either User.IsInRole or ClaimsPrincipal.Current.IsInRole it always returns false.
Click Here to see
The role is being return in the json of Claims in the screenshot above {roles:SuperAdmin}.
I have done alot of reading up and as far as i can see i am doing everything correctly but cant find a reason why?
Below is my Startup.Auth.cs
public partial class Startup
{
private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
private static string appKey = ConfigurationManager.AppSettings["ida:ClientSecret"];
private static string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
private static string tenantId = ConfigurationManager.AppSettings["ida:TenantId"];
private static string postLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"];
public static readonly string Authority = aadInstance + tenantId;
// This is the resource ID of the AAD Graph API. We'll need this to request a token to call the Graph API.
//string graphResourceId = "https://graph.windows.net";
public void ConfigureAuth(IAppBuilder app)
{
ApplicationDbContext db = new ApplicationDbContext();
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = Authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
{
RoleClaimType= "roles"
},
Notifications = new OpenIdConnectAuthenticationNotifications()
{
// If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away.
AuthorizationCodeReceived = (context) =>
{
var code = context.Code;
ClientCredential credential = new ClientCredential(clientId, appKey);
string signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
AuthenticationContext authContext = new AuthenticationContext(Authority, new ADALTokenCache(signedInUserID));
return Task.FromResult(0);
}
}
});
}
}
Since you are using OpenID Connect Owin middleware to sign-in users from Azure AD , you doesn't need to enable App Service Authentication / Authorization feature , that feature provides a way for your application to sign in users so that you don't have to change code on the app backend. Just turn off the App Service Authentication / Authorization feature .
Problem Statement
I am using .NET Core, and I'm trying to make a web application talk to a web API. Both require authentication using the [Authorize] attribute on all of their classes. In order to be able to talk between them server-to-server, I need to retrieve the validation token. I've been able to do that thanks to a Microsoft tutorial.
Problem
In the tutorial, they use a call to AcquireTokenByAuthorizationCodeAsync in order to save the token in the cache, so that in other places, the code can just do a AcquireTokenSilentAsync, which doesn't require going to the Authority to validate the user.
This method does not lookup token cache, but stores the result in it, so it can be looked up using other methods such as AcquireTokenSilentAsync
The issue comes in when the user is already logged in. The method stored at OpenIdConnectEvents.OnAuthorizationCodeReceived never gets called, since there is no authorization being received. That method only gets called when there's a fresh login.
There is another event called: CookieAuthenticationEvents.OnValidatePrincipal when the user is only being validated via a cookie. This works, and I can get the token, but I have to use AcquireTokenAsync, since I don't have the authorization code at that point. According to the documentation, it
Acquires security token from the authority.
This makes calling AcquireTokenSilentAsync fail, since the token has not been cached. And I'd rather not always use AcquireTokenAsync, since that always goes to the Authority.
Question
How can I tell the token gotten by AcquireTokenAsync to be cached so that I can use AcquireTokenSilentAsync everywhere else?
Relevant code
This all comes from the Startup.cs file in the main, Web Application project.
This is how the event handling is done:
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
Events = new CookieAuthenticationEvents()
{
OnValidatePrincipal = OnValidatePrincipal,
}
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
ClientId = ClientId,
Authority = Authority,
PostLogoutRedirectUri = Configuration["AzureAd:PostLogoutRedirectUri"],
ResponseType = OpenIdConnectResponseType.CodeIdToken,
CallbackPath = Configuration["Authentication:AzureAd:CallbackPath"],
GetClaimsFromUserInfoEndpoint = false,
Events = new OpenIdConnectEvents()
{
OnRemoteFailure = OnAuthenticationFailed,
OnAuthorizationCodeReceived = OnAuthorizationCodeReceived,
}
});
And these are the events behind:
private async Task OnValidatePrincipal(CookieValidatePrincipalContext context)
{
string userObjectId = (context.Principal.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier"))?.Value;
ClientCredential clientCred = new ClientCredential(ClientId, ClientSecret);
AuthenticationContext authContext = new AuthenticationContext(Authority, new NaiveSessionCache(userObjectId, context.HttpContext.Session));
AuthenticationResult authResult = await authContext.AcquireTokenAsync(ClientResourceId, clientCred);
// How to store token in authResult?
}
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
{
// Acquire a Token for the Graph API and cache it using ADAL. In the TodoListController, we'll use the cache to acquire a token to the Todo List API
string userObjectId = (context.Ticket.Principal.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier"))?.Value;
ClientCredential clientCred = new ClientCredential(ClientId, ClientSecret);
AuthenticationContext authContext = new AuthenticationContext(Authority, new NaiveSessionCache(userObjectId, context.HttpContext.Session));
AuthenticationResult authResult = await authContext.AcquireTokenByAuthorizationCodeAsync(
context.ProtocolMessage.Code, new Uri(context.Properties.Items[OpenIdConnectDefaults.RedirectUriForCodePropertiesKey]), clientCred, GraphResourceId);
// Notify the OIDC middleware that we already took care of code redemption.
context.HandleCodeRedemption();
}
// Handle sign-in errors differently than generic errors.
private Task OnAuthenticationFailed(FailureContext context)
{
context.HandleResponse();
context.Response.Redirect("/Home/Error?message=" + context.Failure.Message);
return Task.FromResult(0);
}
Any other code can be found in the linked tutorial, or ask and I will add it to the question.
(Note: I had been struggling with this exact issue for several days. I followed the same Microsoft Tutorial as the one linked in the question, and tracked various problems like a wild goose chase; it turns out the sample contains a whole bunch of seemingly unnecessary steps when using the latest version of the Microsoft.AspNetCore.Authentication.OpenIdConnect package.).
I eventually had a breakthrough moment when I read this page:
http://docs.identityserver.io/en/release/quickstarts/5_hybrid_and_api_access.html
The solution essentially involves letting OpenID Connect auth put the various tokens (access_token, refresh_token) into the cookie.
Firstly, I'm using a Converged Application created at https://apps.dev.microsoft.com and v2.0 of the Azure AD endpoint. The App has an Application Secret (password/public key) and uses Allow Implicit Flow for a Web platform.
(For some reason it seems as if v2.0 of the endpoint doesn't work with Azure AD only applications. I'm not sure why, and I'm not sure if it really matters anyway.)
Relevant lines from the Startup.Configure method:
// Configure the OWIN pipeline to use cookie auth.
app.UseCookieAuthentication(new CookieAuthenticationOptions());
// Configure the OWIN pipeline to use OpenID Connect auth.
var openIdConnectOptions = new OpenIdConnectOptions
{
ClientId = "{Your-ClientId}",
ClientSecret = "{Your-ClientSecret}",
Authority = "http://login.microsoftonline.com/{Your-TenantId}/v2.0",
ResponseType = OpenIdConnectResponseType.CodeIdToken,
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
},
GetClaimsFromUserInfoEndpoint = true,
SaveTokens = true,
};
openIdConnectOptions.Scope.Add("offline_access");
app.UseOpenIdConnectAuthentication(openIdConnectOptions);
And that's it! No OpenIdConnectOptions.Event callbacks. No calls to AcquireTokenAsync or AcquireTokenSilentAsync. No TokenCache. None of those things seem to be necessary.
The magic seems to happen as part of OpenIdConnectOptions.SaveTokens = true
Here's an example where I'm using the access token to send an e-mail on behalf of the user using their Office365 account.
I have a WebAPI controller action which obtains their access token using HttpContext.Authentication.GetTokenAsync("access_token"):
[HttpGet]
public async Task<IActionResult> Get()
{
var graphClient = new GraphServiceClient(new DelegateAuthenticationProvider(async requestMessage =>
{
var accessToken = await HttpContext.Authentication.GetTokenAsync("access_token");
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
}));
var message = new Message
{
Subject = "Hello",
Body = new ItemBody
{
Content = "World",
ContentType = BodyType.Text,
},
ToRecipients = new[]
{
new Recipient
{
EmailAddress = new EmailAddress
{
Address = "email#address.com",
Name = "Somebody",
}
}
},
};
var request = graphClient.Me.SendMail(message, true);
await request.Request().PostAsync();
return Ok();
}
Side Note #1
At some point you might also need to get hold of the refresh_token too, in case the access_token expires:
HttpContext.Authentication.GetTokenAsync("refresh_token")
Side Note #2
My OpenIdConnectOptions actually includes a few more things which I've omitted here, for example:
openIdConnectOptions.Scope.Add("email");
openIdConnectOptions.Scope.Add("Mail.Send");
I've used these for working with the Microsoft.Graph API to send an e-mail on behalf of the currently logged in user.
(Those delegated permissions for Microsoft Graph are set up on the app too).
Update - How to 'silently' Refresh the Azure AD Access Token
So far, this answer explains how to use the cached access token but not what to do when the token expires (typically after 1 hour).
The options seem to be:
Force the user to sign in again. (Not silent)
POST a request to the Azure AD service using the refresh_token to obtain a new access_token (silent).
How to Refresh the Access Token using v2.0 of the Endpoint
After more digging, I found part of the answer in this SO Question:
How to handle expired access token in asp.net core using refresh token with OpenId Connect
It seems like the Microsoft OpenIdConnect libraries do not refresh the access token for you. Unfortunately the answer in the question above is missing the crucial detail about precisely how to refresh the token; presumably because it depends on specific details about Azure AD which OpenIdConnect doesn't care about.
The accepted answer to the above question suggests sending a request directly to the Azure AD Token REST API instead of using one of the Azure AD libraries.
Here's the relevant documentation (Note: this covers a mix of v1.0 and v2.0)
https://developer.microsoft.com/en-us/graph/docs/concepts/rest
https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-protocols-oauth-code#refreshing-the-access-tokens
Here's a proxy based on the API docs:
public class AzureAdRefreshTokenProxy
{
private const string HostUrl = "https://login.microsoftonline.com/";
private const string TokenUrl = $"{Your-Tenant-Id}/oauth2/v2.0/token";
private const string ContentType = "application/x-www-form-urlencoded";
// "HttpClient is intended to be instantiated once and re-used throughout the life of an application."
// - MSDN Docs:
// https://msdn.microsoft.com/en-us/library/system.net.http.httpclient(v=vs.110).aspx
private static readonly HttpClient Http = new HttpClient {BaseAddress = new Uri(HostUrl)};
public async Task<AzureAdTokenResponse> RefreshAccessTokenAsync(string refreshToken)
{
var body = $"client_id={Your-Client-Id}" +
$"&refresh_token={refreshToken}" +
"&grant_type=refresh_token" +
$"&client_secret={Your-Client-Secret}";
var content = new StringContent(body, Encoding.UTF8, ContentType);
using (var response = await Http.PostAsync(TokenUrl, content))
{
var responseContent = await response.Content.ReadAsStringAsync();
return response.IsSuccessStatusCode
? JsonConvert.DeserializeObject<AzureAdTokenResponse>(responseContent)
: throw new AzureAdTokenApiException(
JsonConvert.DeserializeObject<AzureAdErrorResponse>(responseContent));
}
}
}
The AzureAdTokenResponse and AzureAdErrorResponse classes used by JsonConvert:
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
public class AzureAdTokenResponse
{
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "token_type", Required = Required.Default)]
public string TokenType { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "expires_in", Required = Required.Default)]
public int ExpiresIn { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "expires_on", Required = Required.Default)]
public string ExpiresOn { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "resource", Required = Required.Default)]
public string Resource { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "access_token", Required = Required.Default)]
public string AccessToken { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "refresh_token", Required = Required.Default)]
public string RefreshToken { get; set; }
}
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
public class AzureAdErrorResponse
{
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "error", Required = Required.Default)]
public string Error { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "error_description", Required = Required.Default)]
public string ErrorDescription { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "error_codes", Required = Required.Default)]
public int[] ErrorCodes { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "timestamp", Required = Required.Default)]
public string Timestamp { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "trace_id", Required = Required.Default)]
public string TraceId { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "correlation_id", Required = Required.Default)]
public string CorrelationId { get; set; }
}
public class AzureAdTokenApiException : Exception
{
public AzureAdErrorResponse Error { get; }
public AzureAdTokenApiException(AzureAdErrorResponse error) :
base($"{error.Error} {error.ErrorDescription}")
{
Error = error;
}
}
Finally, my modifications to Startup.cs to refresh the access_token
(Based on the answer I linked above)
// Configure the OWIN pipeline to use cookie auth.
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = OnValidatePrincipal
},
});
The OnValidatePrincipal handler in Startup.cs (Again, from the linked answer above):
private async Task OnValidatePrincipal(CookieValidatePrincipalContext context)
{
if (context.Properties.Items.ContainsKey(".Token.expires_at"))
{
if (!DateTime.TryParse(context.Properties.Items[".Token.expires_at"], out var expiresAt))
{
expiresAt = DateTime.Now;
}
if (expiresAt < DateTime.Now.AddMinutes(-5))
{
var refreshToken = context.Properties.Items[".Token.refresh_token"];
var refreshTokenService = new AzureAdRefreshTokenService();
var response = await refreshTokenService.RefreshAccessTokenAsync(refreshToken);
context.Properties.Items[".Token.access_token"] = response.AccessToken;
context.Properties.Items[".Token.refresh_token"] = response.RefreshToken;
context.Properties.Items[".Token.expires_at"] = DateTime.Now.AddSeconds(response.ExpiresIn).ToString(CultureInfo.InvariantCulture);
context.ShouldRenew = true;
}
}
}
Finally, a solution with OpenIdConnect using v2.0 of the Azure AD API.
Interestingly, it seems that v2.0 does not ask for a resource to be included in the API request; the documentation suggests it's necessary, but the API itself simply replies that resource is not supported. This is probably a good thing - presumably it means that the access token works for all resources (it certainly works with the Microsoft Graph API)
How to refresh Authentication token for
Microsoft Graph using Microsoft Graph .NET Client Library or other using C#?
What I am currently doing is keeping token in the static class:
public class TokenKeeper
{
public static string token = null;
public static string AcquireToken()
{
if (token == null || token.IsEmpty())
{
throw new Exception("Authorization Required.");
}
return token;
}
public static void Clear()
{
token = null;
}
}
I fill in the token in Startup class:
public partial class Startup
{
private static string AppKey = CloudConfigurationManager.GetSetting("ida:Password");
private static string aadInstance = CloudConfigurationManager.GetSetting("ida:AADInstance");
private static string TenantName = CloudConfigurationManager.GetSetting("ida:Tenant");
private static string Authority = String.Format(CultureInfo.InvariantCulture, aadInstance, TenantName);
private static string graphResourceId = CloudConfigurationManager.GetSetting("ida:GraphUrl");
private BpContext db = new BpContext();
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
public void ConfigureAuth(IAppBuilder app)
{
string ClientId = CloudConfigurationManager.GetSetting("ida:ClientID");
string Authority = "https://login.microsoftonline.com/common/";
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = ClientId,
Authority = Authority,
Scope = "User.ReadBasic.All",
//Details omitted
AuthorizationCodeReceived = (context) =>
{
var code = context.Code;
// Create a Client Credential Using an Application Key
ClientCredential credential = new ClientCredential(ClientId, AppKey);
string userObjectID = context.AuthenticationTicket.Identity.FindFirst(
"http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
AuthenticationContext authContext = new AuthenticationContext(Authority, new NaiveSessionCache(userObjectID));
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(
code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceId);
TokenKeeper.token = result.AccessToken;
return Task.FromResult(0);
}
//Details omitted
}
});
}
}
I also clear the token on Sign Out.
The AuthenticationResult object contains both access token and refresh token. So, the refresh token can also be persisted in TokenKeeper similar to access token. When access token expires (indicated by AuthenticationResult.ExpiresOn), use the refresh token with AuthenticationContext.AcquireTokenByRefreshToken method to get new access token.
If you don't want to track refresh tokens explicitly, please refer to ADAL Cache to know how ADAL library can do it for you.
You can refresh access token by providing RefreshToken which you received alongside AccessToken. Since you have ID/Secret available in you code you can use them to provide ClientCredential.
Code example would be:
var authContext = new AuthenticationContext("https://login.microsoftonline.com/common");
var result = authContext.AcquireTokenByRefreshToken(refreshToken, new ClientCredential(ClientId, AppKey));
I apologize in advance for asking this as I have next to no knowledge of security in general and IdentityServer in particular.
I am trying to set up IdentityServer to manage security for an Asp.Net MVC application.
I am following the tutorial on their website: Asp.Net MVC with IdentityServer
However, I am doing something slightly different in that I have a separate project for the Identity "Server" part, which leads to 2 Startup.cs files, one for the application and one for the Identity Server
For the application, the Startup.cs file looks like this
public class Startup
{
public void Configuration(IAppBuilder app)
{
AntiForgeryConfig.UniqueClaimTypeIdentifier = Constants.ClaimTypes.Subject;
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44301/identity",
ClientId = "baseballStats",
Scope = "openid profile roles baseballStatsApi",
RedirectUri = "https://localhost:44300/",
ResponseType = "id_token token",
SignInAsAuthenticationType = "Cookies",
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = async n =>
{
var userInfoClient = new UserInfoClient(
new Uri(n.Options.Authority + "/connect/userinfo"),
n.ProtocolMessage.AccessToken);
var userInfo = await userInfoClient.GetAsync();
// create new identity and set name and role claim type
var nid = new ClaimsIdentity(
n.AuthenticationTicket.Identity.AuthenticationType,
Constants.ClaimTypes.GivenName,
Constants.ClaimTypes.Role);
userInfo.Claims.ToList().ForEach(c => nid.AddClaim(new Claim(c.Item1, c.Item2)));
// keep the id_token for logout
nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
// add access token for sample API
nid.AddClaim(new Claim("access_token", n.ProtocolMessage.AccessToken));
// keep track of access token expiration
nid.AddClaim(new Claim("expires_at", DateTimeOffset.Now.AddSeconds(int.Parse(n.ProtocolMessage.ExpiresIn)).ToString()));
// add some other app specific claim
nid.AddClaim(new Claim("app_specific", "some data"));
n.AuthenticationTicket = new AuthenticationTicket(
nid,
n.AuthenticationTicket.Properties);
}
}
});
app.UseResourceAuthorization(new AuthorizationManager());
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost:44301/identity",
RequiredScopes = new[] { "baseballStatsApi"}
});
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
app.UseWebApi(config);
}
}
For the identity server, the startup.cs file is
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.Map("/identity", idsrvApp =>
{
idsrvApp.UseIdentityServer(new IdentityServerOptions
{
SiteName = "Embedded IdentityServer",
SigningCertificate = LoadCertificate(),
Factory = InMemoryFactory.Create(
users: Users.Get(),
clients: Clients.Get(),
scopes: Scopes.Get())
});
});
}
X509Certificate2 LoadCertificate()
{
return new X509Certificate2(
string.Format(#"{0}\bin\Configuration\idsrv3test.pfx", AppDomain.CurrentDomain.BaseDirectory), "idsrv3test");
}
}
I am also setting up an Authorization Manager
public class AuthorizationManager : ResourceAuthorizationManager
{
public override Task<bool> CheckAccessAsync(ResourceAuthorizationContext context)
{
switch (context.Resource.First().Value)
{
case "Players":
return CheckAuthorization(context);
case "About":
return CheckAuthorization(context);
default:
return Nok();
}
}
private Task<bool> CheckAuthorization(ResourceAuthorizationContext context)
{
switch(context.Action.First().Value)
{
case "Read":
return Eval(context.Principal.HasClaim("role", "LevelOneSubscriber"));
default:
return Nok();
}
}
}
So for instance, if I define a controller method that is decorated with the ResourceAuthorize attribute, like so
public class HomeController : Controller
{
[ResourceAuthorize("Read", "About")]
public ActionResult About()
{
return View((User as ClaimsPrincipal).Claims);
}
}
Then, when I first try to access this method, I will be redirected to the default login page.
What I don't understand however, is why when I login with the user I have defined for the application (see below),
public class Users
{
public static List<InMemoryUser> Get()
{
return new List<InMemoryUser>
{
new InMemoryUser
{
Username = "bob",
Password = "secret",
Subject = "1",
Claims = new[]
{
new Claim(Constants.ClaimTypes.GivenName, "Bob"),
new Claim(Constants.ClaimTypes.FamilyName, "Smith"),
new Claim(Constants.ClaimTypes.Role, "Geek"),
new Claim(Constants.ClaimTypes.Role, "LevelOneSubscriber")
}
}
};
}
}
I get a 403 error, Bearer error="insufficient_scope".
Can anybody explain what I am doing wrong?
Any subsequent attempt to access the action method will return the same error. It seems to me that the user I defined has the correct claims to be able to access this method. However, the claims check only happens once, when I first try to access this method. After I login I get a cookie, and the claims check is not made during subsequent attempts to access the method.
I'm a bit lost, and would appreciate some help in clearing this up.
Thanks in advance.
EDIT: here are the scoles and client classes
public static class Scopes
{
public static IEnumerable<Scope> Get()
{
var scopes = new List<Scope>
{
new Scope
{
Enabled = true,
Name = "roles",
Type = ScopeType.Identity,
Claims = new List<ScopeClaim>
{
new ScopeClaim("role")
}
},
new Scope
{
Enabled = true,
Name = "baseballStatsApi",
Description = "Access to baseball stats API",
Type = ScopeType.Resource,
Claims = new List<ScopeClaim>
{
new ScopeClaim("role")
}
}
};
scopes.AddRange(StandardScopes.All);
return scopes;
}
}
And the Client class
public static class Clients
{
public static IEnumerable<Client> Get()
{
return new[]
{
new Client
{
Enabled = true,
ClientName = "Baseball Stats Emporium",
ClientId = "baseballStats",
Flow = Flows.Implicit,
RedirectUris = new List<string>
{
"https://localhost:44300/"
}
},
new Client
{
Enabled = true,
ClientName = "Baseball Stats API Client",
ClientId = "baseballStats_Api",
ClientSecrets = new List<ClientSecret>
{
new ClientSecret("secret".Sha256())
},
Flow = Flows.ClientCredentials
}
};
}
}
I have also created a custom filter attribute which I use to determine when the claims check is made.
public class CustomFilterAttribute : ResourceAuthorizeAttribute
{
public CustomFilterAttribute(string action, params string[] resources) : base(action, resources)
{
}
protected override bool CheckAccess(HttpContextBase httpContext, string action, params string[] resources)
{
return base.CheckAccess(httpContext, action, resources);
}
}
The breakpoint is hit only on the initial request to the url. On subsequent requests, the filter attribute breakpoint is not hit, and thus no check occurs. This is surprising to me as I assumed the check would have to be made everytime the url is requested.
You need to request the scopes required by the api when the user logs in.
Scope = "openid profile roles baseballStatsApi"
Authority = "https://localhost:44301/identity",
ClientId = "baseballStats",
Scope = "openid profile roles baseballStatsApi",
ResponseType = "id_token token",
RedirectUri = "https://localhost:44300/",
SignInAsAuthenticationType = "Cookies",
UseTokenLifetime = false,