I have to connect my existing MVC project written in .net 4.5 to IdentityServer4. Yesterday I tried this solution: https://stackoverflow.com/a/39771372, and it's working, but I need to handle callback and load additional user data from client app database. I've tried to change RedirectUri to my own Controller method, but it is not called.
Today I trying to use Implicit Flow from sample: https://github.com/IdentityServer/IdentityServer4.Samples/tree/release/Clients/src/MvcManual - i know that sample is net.core, but now I can handle callback from IdentityServer. Now how can I validate token? I have problem with whole ValidateIdentityToken method
I decoded token using:
var token = new JwtSecurityToken(jwtEncodedString: idToken);
In token I found my user info, but I think it is not safe method.
Finally I have two questions:
Can I simply configure my client to redirect to my controller method where I loading additional user data like "sitemap" from Database? It is possible using first sample?
Using second sample, anyone knows how to validate token in net4.5 client?
After validation I need only Subject and username to load additional data.
EDIT: My configuration
IdentityServer4 client configuration:
a) Hybrid
new Client
{
ClientName = "mvc Client",
ClientId = "mvc",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
RedirectUris = { "http://localhost:5002/Account/Callback" }, // where to redirect to after login
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" }, // where to redirect to after logout
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api"
},
ClientSecrets =
{
new Secret("secret".Sha256())
},
RequireConsent = false,
AlwaysIncludeUserClaimsInIdToken = true
}
b) Implicit method
new Client
{
ClientName = "mvc Client",
ClientId = "mvc",
AllowedGrantTypes = {GrantType.ClientCredentials, GrantType.Implicit},
RedirectUris = { "http://localhost:5002/Account/Callback" }, // where to redirect to after login
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" }, // where to redirect to after logout
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api"
},
ClientSecrets =
{
new Secret("secret".Sha256())
},
RequireConsent = false,
AlwaysIncludeUserClaimsInIdToken = true
}
MVC .net4.5 configuration
a) using Microsoft.Owin.Security.OpenIdConnect, where RedirectUri is not called
Startup.cs
public void ConfigureAuthentication(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "http://localhost:5000", //ID Server
ClientId = "mvc",
ResponseType = "id_token code",
SignInAsAuthenticationType = "Cookies",
RedirectUri = "http://localhost:5002/Account/Callback", //URL of website
Scope = "openid profile",
});
}
b) Using second method Implicit
Startup.cs
public void ConfigureAuthentication(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies",
LoginPath = new PathString("/Account/Secure")
});
}
AccountController
public class AccountController : BaseController
{
[AllowAnonymous]
public async Task<ActionResult> Secure()
{
return await StartAuthentication();
}
public async Task<ActionResult> Logout()
{
Authentication.SignOut("Cookies");
var disco = await DiscoveryClient.GetAsync(Constants.Authority);
return Redirect(disco.EndSessionEndpoint);
}
[AllowAnonymous]
private async Task<ActionResult> StartAuthentication()
{
// read discovery document to find authorize endpoint
var disco = await DiscoveryClient.GetAsync(Constants.Authority);
var authorizeUrl = new AuthorizeRequest(disco.AuthorizeEndpoint).CreateAuthorizeUrl(
clientId: "mvc",
responseType: "id_token",
scope: "openid profile",
redirectUri: "http://localhost:5002/Account/Callback",
state: "random_state",
nonce: "random_nonce",
responseMode: "form_post");
return Redirect(authorizeUrl);
}
public async Task<ActionResult> Callback()
{
var state = Request.Form["state"].ToString();
var idToken = Request.Form["id_token"].ToString();
if (!string.Equals(state, "random_state")) throw new Exception("invalid state");
var user = await ValidateIdentityToken(idToken, state);
var id = new ClaimsIdentity(user, "Cookies");
Authentication.SignIn(new AuthenticationProperties
{
IsPersistent = false
}, id);
//Here Loading other data for user from Database e.g SiteMap
// {...}
return Redirect("/");
}
private async Task<ClaimsPrincipal> ValidateIdentityToken(string idToken)
{
// working area
var token = new JwtSecurityToken(jwtEncodedString: idToken);
Console.WriteLine("email => " + token.Claims.First(c => c.Type == "email").Value);
//working area
//need to return user Claims from validated token. How to?
}
BaseController
[Authorize]
public abstract class BaseController : Controller
{
public IAuthenticationManager Authentication
{
get { return this.HttpContext.GetOwinContext().Authentication; }
}
// Methods that can be used in my other controllers.
}
Related
I have the following code in my Startup.cs
var openIdOptions = new OpenIdConnectAuthenticationOptions
{
ClientId = openIdConfiguration.ClientId,
Authority = openIdConfiguration.Authority,
RedirectUri = openIdConfiguration.RedirectUri,
ResponseType = openIdConfiguration.CodeFlowEnabled ? "code" : OpenIdConnectResponseTypes.IdToken,
TokenValidationParameters = tokenValidationParameters,
Scope = string.IsNullOrEmpty(openIdConfiguration.Scope) ? OpenIdConnectScopes.OpenIdProfile : openIdConfiguration.Scope,
PostLogoutRedirectUri = openIdConfiguration.PostLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = this.AuthorizationCodeReceived,
MessageReceived = this.MessageReceived,
SecurityTokenValidated = this.SecurityTokenValidated,
SecurityTokenReceived = this.SecurityTokenReceived,
AuthenticationFailed = this.AuthenticationFailed,
RedirectToIdentityProvider = this.RedirectToIdentityProvider
},
SignInAsAuthenticationType = "Cookies",
CookieManager = new SameSiteCookieManager(new SystemWebCookieManager())
};
....
app.UseOpenIdConnectAuthentication(openIdOptions);
I would like to generate a token from a separate TokenService.cs, with a different configuration which will be consumed by another application.
public class TokenService
{
public async Task<string> GenerateToken()
{
var openIdOptions = new OpenIdConnectAuthenticationOptions
{
//similar OIDC config
}
**// How to generate a token ?**
return token;
}
}
How to go about generating a new token when needed from my TokenService class ?
I'm using Azure AD Connection for integrated login in an ASP.Net website.
If I use the same code and same configurations in a brand new project, it works. When implementing in my main project, it has some unusual behaviour.
Startup.cs is the following:
public class startup
{
string clientId = System.Configuration.ConfigurationManager.AppSettings["ClientId"];
string redirectUri = System.Configuration.ConfigurationManager.AppSettings["RedirectUri"];
static string tenant = System.Configuration.ConfigurationManager.AppSettings["Tenant"];
string authority = String.Format(System.Globalization.CultureInfo.InvariantCulture, System.Configuration.ConfigurationManager.AppSettings["Authority"], tenant);
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
Scope = OpenIdConnectScope.OpenIdProfile,
ResponseType = OpenIdConnectResponseType.IdToken,
TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = false
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed
}
}
);
}
private Task OnAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> context)
{
context.HandleResponse();
context.Response.Redirect("/?errormessage=" + context.Exception.Message);
return Task.FromResult(0);
}
}
My c# code in login page:
protected void btnLogin_Click(object sender, EventArgs e)
{
SignIn();
}
public void SignIn()
{
if (!Request.IsAuthenticated)
{
HttpContext.Current.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties { RedirectUri = "/MainAzure.aspx" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
else
Response.Redirect("MainAzure.aspx");
}
The issue is this:
When I Press the login button in my aspx page, it opens the Microsoft login page. When I select the account and enter the password, it redirects to my redirect page (MainAzure.aspx) but without the authentication properties. Request.IsAuthenticated returns false.
Any hints?
Try changing your implementation as below approach:
Startup.cs :
public class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
/*
* Configure the OWIN middleware
*/
public void ConfigureAuth(IAppBuilder app)
{
// Required for Azure webapps, as by default they force TLS 1.2 and this project attempts 1.0
..ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
// Generate the metadata address using the tenant and policy information
MetadataAddress = String.Format(Globals.WellKnownMetadata, Globals.Tenant, Globals.DefaultPolicy),
// These are standard OpenID Connect parameters, with values pulled from web.config
ClientId = Globals.ClientId,
RedirectUri = Globals.RedirectUri,
PostLogoutRedirectUri = Globals.RedirectUri,
// Specify the callbacks for each type of notifications
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = OnRedirectToIdentityProvider,
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
AuthenticationFailed = OnAuthenticationFailed,
},
// Specify the claim type that specifies the Name property.
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
ValidateIssuer = false
},
// Specify the scope by appending all of the scopes requested into one string (separated by a blank space)
Scope = $"openid profile offline_access {Globals.ReadTasksScope} {Globals.WriteTasksScope}"
}
);
}
/*
* On each call to Azure AD B2C, check if a policy (e.g. the profile edit or password reset policy) has been specified in the OWIN context.
* If so, use that policy when making the call. Also, don't request a code (since it won't be needed).
*/
private Task OnRedirectToIdentityProvider(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
var policy = notification.OwinContext.Get<string>("Policy");
if (!string.IsNullOrEmpty(policy) && !policy.Equals(Globals.DefaultPolicy))
{
notification.ProtocolMessage.Scope = OpenIdConnectScope.OpenId;
notification.ProtocolMessage.ResponseType = OpenIdConnectResponseType.IdToken;
notification.ProtocolMessage.IssuerAddress = notification.ProtocolMessage.IssuerAddress.ToLower().Replace(Globals.DefaultPolicy.ToLower(), policy.ToLower());
}
return Task.FromResult(0);
}
/*
* Catch any failures received by the authentication middleware and handle appropriately
*/
private Task OnAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
notification.HandleResponse();
// Handle the error code that Azure AD B2C throws when trying to reset a password from the login page
// because password reset is not supported by a "sign-up or sign-in policy"
if (notification.ProtocolMessage.ErrorDescription != null && notification.ProtocolMessage.ErrorDescription.Contains("AADB2C90118"))
{
// If the user clicked the reset password link, redirect to the reset password route
notification.Response.Redirect("/Account/ResetPassword");
}
else if (notification.Exception.Message == "access_denied")
{
notification.Response.Redirect("/");
}
else
{
notification.Response.Redirect("/Home/Error?message=" + notification.Exception.Message);
}
return Task.FromResult(0);
}
/*
* Callback function when an authorization code is received
*/
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
{
try
{
/*
The `MSALPerUserMemoryTokenCache` is created and hooked in the `UserTokenCache` used by `IConfidentialClientApplication`.
At this point, if you inspect `ClaimsPrinciple.Current` you will notice that the Identity is still unauthenticated and it has no claims,
but `MSALPerUserMemoryTokenCache` needs the claims to work properly. Because of this sync problem, we are using the constructor that
receives `ClaimsPrincipal` as argument and we are getting the claims from the object `AuthorizationCodeReceivedNotification context`.
This object contains the property `AuthenticationTicket.Identity`, which is a `ClaimsIdentity`, created from the token received from
Azure AD and has a full set of claims.
*/
IConfidentialClientApplication confidentialClient = MsalAppBuilder.BuildConfidentialClientApplication(new ClaimsPrincipal(notification.AuthenticationTicket.Identity));
// Upon successful sign in, get & cache a token using MSAL
AuthenticationResult result = await confidentialClient.AcquireTokenByAuthorizationCode(Globals.Scopes, notification.Code).ExecuteAsync();
}
catch (Exception ex)
{
throw new HttpResponseException(new HttpResponseMessage
{
StatusCode = HttpStatusCode.BadRequest,
ReasonPhrase = $"Unable to get authorization code {ex.Message}."
});
}
}
(For user authentication activities) AccountController.cs :
public class AccountController : Controller
{
/*
* Called when requesting to sign up or sign in
*/
public void SignUpSignIn(string redirectUrl)
{
redirectUrl = redirectUrl ?? "/";
// Use the default policy to process the sign up / sign in flow
HttpContext.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = redirectUrl });
return;
}
/*
* Called when requesting to edit a profile
*/
public void EditProfile()
{
if (Request.IsAuthenticated)
{
// Let the middleware know you are trying to use the edit profile policy (see OnRedirectToIdentityProvider in Startup.Auth.cs)
HttpContext.GetOwinContext().Set("Policy", Globals.EditProfilePolicyId);
// Set the page to redirect to after editing the profile
var authenticationProperties = new AuthenticationProperties { RedirectUri = "/" };
HttpContext.GetOwinContext().Authentication.Challenge(authenticationProperties);
return;
}
Response.Redirect("/");
}
/*
* Called when requesting to reset a password
*/
public void ResetPassword()
{
// Let the middleware know you are trying to use the reset password policy (see OnRedirectToIdentityProvider in Startup.Auth.cs)
HttpContext.GetOwinContext().Set("Policy", Globals.ResetPasswordPolicyId);
// Set the page to redirect to after changing passwords
var authenticationProperties = new AuthenticationProperties { RedirectUri = "/" };
HttpContext.GetOwinContext().Authentication.Challenge(authenticationProperties);
return;
}
/*
* Called when requesting to sign out
*/
public async Task SignOut()
{
// To sign out the user, you should issue an OpenIDConnect sign out request.
if (Request.IsAuthenticated)
{
await MsalAppBuilder.ClearUserTokenCache();
IEnumerable<AuthenticationDescription> authTypes = HttpContext.GetOwinContext().Authentication.GetAuthenticationTypes();
HttpContext.GetOwinContext().Authentication.SignOut(authTypes.Select(t => t.AuthenticationType).ToArray());
Request.GetOwinContext().Authentication.GetAuthenticationTypes();
}
}
}
For complete code, you can visit Azure AD B2C: Call an ASP.NET Web API from an ASP.NET Web App
I am using IdentityModel.AspNetCore and .AddClientAccessTokenHandler() extension to automatically supply HttpClient with access token (at least that is what I understand I can use it for) to an API. Some API endpoints are authorized based on a role. But for some reason, the access token that is added to the request does not contain the role claim. If I do not use the .AddClientAccessTokenHandler() and manually retrieve the token and set it using SetBearerToken(accessTone) then I can reach my role authorized endpoint.
My startup is:
services.AddAccessTokenManagement(options =>
{
options.Client.Clients.Add("auth", new ClientCredentialsTokenRequest
{
Address = "https://localhost:44358/connect/token",
ClientId = "clientId",
ClientSecret = "clientSecret",
});
});
WebApi call:
var response = await _httpClient.GetAsync("api/WeatherForecast/SecretRole");
Identity server configuration:
public static IEnumerable<ApiResource> GetApis() =>
new List<ApiResource>
{
new ApiResource("WebApi", new string[] { "role" })
{ Scopes = { "WebApi.All" }}
};
public static IEnumerable<ApiScope> GetApiScopes() =>
new List<ApiScope>
{ new ApiScope("WebApi.All") };
public static IEnumerable<IdentityResource> GetIdentityResources() =>
new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResource
{
Name = "roles",
UserClaims = { "role" }
}
};
public static IEnumerable<Client> GetClients() =>
new List<Client>
{
new Client
{
ClientId = "clientId",
ClientSecrets = { new Secret("clientSecret".ToSha256()) },
AllowedGrantTypes =
{
GrantType.AuthorizationCode,
GrantType.ClientCredentials
},
AllowedScopes =
{
"WebApi.All",
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"roles"
},
RedirectUris = { "https://localhost:44305/signin-oidc" },
PostLogoutRedirectUris = { "https://localhost:44305/Home/Index" },
AlwaysIncludeUserClaimsInIdToken = false,
AllowOfflineAccess = true,
}
};
For testing purposes I add users manually from Program.cs
public static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<IdentityUser>>();
AddUsers(userManager).GetAwaiter().GetResult();
}
host.Run();
}
private static async Task AddUsers(UserManager<IdentityUser> userManager)
{
var adminClaim = new Claim("role", "Admin");
var visitorClaim = new Claim("role", "Visitor");
var user = new IdentityUser("Admin");
await userManager.CreateAsync(user, user.UserName);
await userManager.AddClaimAsync(user, adminClaim);
user = new IdentityUser("Visitor");
await userManager.CreateAsync(user, user.UserName);
await userManager.AddClaimAsync(user, visitorClaim);
}
So if I use manual access token retrieval and add it myself to the HttpClient headers, then my endpoint is reached and returns expected response. If I use .AddClientAccessTokenHandler(), I get 403 - Forbidden. What am I missing?
Since you are registering the client under the name auth, you also should retrieve it as such.
This basically means I expect you to use something like this, or it's equivalent:
_httpClient = factory.CreateClient("auth");
Basically this mechanism ensures you're able to retrieve HttpClients for various API's and settings.
ps. I am on mobile; and currently not very good access to my resources.
I am having trouble getting IdentityServer to return any claims on a resource scope. I define the resource scope as:
private static Scope Roles
{
get
{
return new Scope
{
Name = "roles",
DisplayName = "Roles",
Type = ScopeType.Identity,
Claims = new List<ScopeClaim>
{
new ScopeClaim(SecurityContants.ClaimTypes.Role)
}
};
}
}
private static Scope Api
{
get
{
return new Scope
{
Name = "api",
DisplayName = "Api",
IncludeAllClaimsForUser = true,
//i've also tried adding the claim directly
Type = ScopeType.Resource,
Emphasize = false
};
}
}
Then they're added to a collection and handed back.
In my API Startup.cs, I configure owin like:
public void Configuration (IAppBuilder app)
{
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseIdentityServerBearerTokenAuthentication(
new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "http://localhost/security.tokenserver.site",
RequiredScopes = new[] { "api", "roles" },
RoleClaimType = "roles"
});
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
app.UseWebApi(config);
}
}
My API endpoint is simply:
public IEnumerable<string> Get()
{
var user = User as ClaimsPrincipal;
var claims = new List<string>();
foreach (var item in user.Claims)
{
claims.Add(item.Value);
}
return claims;
}
The output of this, when called from my UI after authenticating is:
[ "customer_svc", "api", "https://idsrv3/embedded", "https://idsrv3/embedded/resources", "1448048492", "1448044892" ]
So the initial login at the UI works (i see the roles claim there), retrieving the token works, and handing the token up to the API works. But I cannot see the user's roles after the token is handed back up to the API.
What am I missing?
I am trying to figure out how to get the claim out of my token.
I will try an keep the explanation short
I have an HTML page that does a post to my web api, does and auth
check and returns an JWT token
when i get the token back i want to send it to different url, and the way i am doing it is using a querystring. I know i can use cookies but for this app we dont want to use them. So if my url looks like this http://somedomain/checkout/?token=bearer token comes here
I am using Owin middleware and this is what i have so far
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
{
Provider = new ApplicationOAuthBearerAuthenticationProvider(),
});
public class ApplicationOAuthBearerAuthenticationProvider
: OAuthBearerAuthenticationProvider
{
public override Task RequestToken(OAuthRequestTokenContext context)
{
if (context == null)
throw new ArgumentNullException("context");
var token = HttpContext.Current.Request.QueryString["token"];
if (!string.IsNullOrEmpty(token))
context.Token = token;
return Task.FromResult<object>(null);
}
}
But how do i get the Claims out of the Token or just check the IsAuthenticated
I tried the Following inside my controller just to check, but the IsAuthenticated is always false
var identity = (ClaimsIdentity) HttpContext.Current.GetOwinContext().Authentication.User.Identity;
if (!identity.IsAuthenticated)
return;
var id = identity.FindFirst(ClaimTypes.NameIdentifier);
OK so I managed to figure it out. The above code that I had is all working well but I needed to add the UseJwtBearerAuthentication middle ware.
One thing I did end up changing from my original code was i changed the context.Token = token; to context.Request.Headers.Add("Authorization", new[] { string.Format("Bearer {0}", token) });
So my startup class looks like this...
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
{
Provider = new ApplicationOAuthBearerAuthenticationProvider(),
});
app.UseJwtBearerAuthentication(JwtOptions());
ConfigureAuth(app);
}
private static JwtBearerAuthenticationOptions JwtOptions()
{
var key = Encoding.UTF8.GetBytes(ConfigurationManager.AppSettings["auth:key"]);
var jwt = new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = Some Audience,
ValidIssuer = Some Issuer,
IssuerSigningToken = new BinarySecretSecurityToken(key),
RequireExpirationTime = false,
ValidateLifetime = false
}
};
return jwt;
}
public class ApplicationOAuthBearerAuthenticationProvider
: OAuthBearerAuthenticationProvider
{
public override Task RequestToken(OAuthRequestTokenContext context)
{
if (context == null)
throw new ArgumentNullException("context");
var token = HttpContext.Current.Request.QueryString["token"];
if (!string.IsNullOrEmpty(token))
context.Request.Headers.Add("Authorization", new[] { string.Format("Bearer {0}", token) });
return Task.FromResult<object>(null);
}
}
}