IdentityServer4 Register from a Client Application - asp.net-core

I have created an OAUth server using IdentityServer4 together with .Net Core Identity. It works fine for the most part. The Startup code is as follows:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = Configuration["Settings:OAuthAddress"];
options.ClientId = "razor_code";
options.ClientSecret = Configuration["Settings:ClientSecret"];
options.ResponseType = "id_token code";
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("myapi");
options.Scope.Add("offline_access");
options.Scope.Add("email");
options.Scope.Add("profile");
options.SaveTokens = true;
});
services.AddAuthorization();
...
}
If I use the [Authorize] attribute on a page in my client site, it will automatically send the user to the OAUth server to login. What I would like to do is to be able to use the Register and Login buttons to login the client app as well. I figured out the Login by creating a Login page as follows:
[Authorize]
public class LoginModel : PageModel
{
public IActionResult OnGetAsync()
{
return LocalRedirect("/");
}
}
If a link points to this page, the [Authorize] attribute will cause it to trigger a login on the OAuth server. Does anyone know a better way to trigger a login? My solution always returns to the root page of the site. This will do, since the root page on this app is the only one without the [Authorize] attribute. In the future that could change.
I also want the Register link to work as well. I found this https://openid.net/specs/openid-connect-prompt-create-1_0.html which indicates that the ability to go straight to the Registration page is at least in the works. Does anyone know how to make this happen using Microsoft.AspNetCore.Authentication.OpeIdConnect?
I know this is technically two questions, but I am thinking the answers are related.

Related

Blazor login path for multiple authentication schemes (AAD and AB2C)

I want to use Azure AD and Azure B2C in one application. I registered both authentication schemes which works fine.
Now I want to allow the user to decide which scheme should be used by clicking a "login with AD" or "login with B2C" button. Clicking on one of the buttons should redirect the user to the correct login.
I'm able to do this for AD by using the link MicrosoftIdentity/Account/SignIn. To do this, it's necessary to use services.AddControllersWithViews().AddMicrosoftIdentityUI()
So, how do I get a link like above for B2C?
Here is my code:
public static void AddAzureADAuthenticationApp(this IServiceCollection services, IConfigurationSection configuration)
{
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
configuration.Bind(options);
options.Events.OnTokenValidated = async context =>
{
await AuthorizationHelper.ValidateAADAppToken(context);
};
})
.EnableTokenAcquisitionToCallDownstreamApi().AddInMemoryTokenCaches();
services.AddControllersWithViews(options =>
{
var policy = new AuthorizationPolicyBuilder(OpenIdConnectDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.AddRequirements(new AzureADAuthorizationRequirement()).Build();
options.Filters.Add(new AuthorizeFilter(policy));
})
.AddMicrosoftIdentityUI();
services.AddAuthorization(config =>
{
var policy = new AuthorizationPolicyBuilder(OpenIdConnectDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.AddRequirements(new AzureADAuthorizationRequirement()).Build();
config.AddPolicy(Constants.PolicyInternalUsers, policy);
});
}
public static void AddAzureB2CAuthenticationApp(this IServiceCollection services, IConfigurationSection configuration, bool RequireAccountNum = false)
{
services.AddAuthentication()
.AddMicrosoftIdentityWebApp(options =>
{
configuration.Bind(options);
options.SignInScheme = Constants.B2CAuthenticationScheme;
options.ResponseType = "code id_token";
options.Scope.Clear();
options.Scope.Add("https://graph.microsoft.com/openid");
options.Events.OnTokenValidated = async context =>
{
await AuthorizationHelper.ValidateB2CAppToken(context, RequireAccountNum);
};
}, openIdConnectScheme: Constants.B2CAuthenticationScheme, cookieScheme: Constants.B2CCookieScheme)
.EnableTokenAcquisitionToCallDownstreamApi().AddInMemoryTokenCaches();
services.AddControllersWithViews(options =>
{
var policy = new AuthorizationPolicyBuilder(FischerLib.Extensions.Constants.B2CAuthenticationScheme)
.RequireAuthenticatedUser()
.AddRequirements(new AzureB2CAuthorizationRequirement(RequireAccountNum)).Build();
options.Filters.Add(new AuthorizeFilter(policy));
})
.AddMicrosoftIdentityUI();
services.AddAuthorization(config =>
{
var policy = new AuthorizationPolicyBuilder(FischerLib.Extensions.Constants.B2CAuthenticationScheme)
.RequireAuthenticatedUser()
.AddRequirements(new AzureB2CAuthorizationRequirement(RequireAccountNum)).Build();
config.AddPolicy(Constants.PolicyExternalUsers, policy);
});
}
At the moment I'm using two controller which have authorize attributes restricted to the scheme. Accessing these controller redirects the user to the login.
But this is not the way which I want to use. After the code above is part of a library and I would like to avoid having to ad the controller in every project.
I know that there is a property LoginPath if I use a different scheme like cookies but I can't set this property in AddMicrosoftIdentityWebApp(...)
Thank you very much!
Markus
Please check this blog on Building a Web Application that Supports both Azure AD and Azure AD B2C - MikaBerglund.com
which works on how to enable switching between Azure AD and Azure AD
B2C just by changing the configuration i.e;app settings.json file
where authority , clientId changes.
The controller actions need to route to selected auth schemes when required.
The way you have added controller seems to be the way.
You can raise a support request for the same.
Reference:
github ref

Can I use openid connect authentication and jwtbearer authentication scheme in one app?

I have an ASP.NET Core web app where the front-end is built in react and for the back-end, I have web APIs, both the things are placed in one web project.
for authentication, I used Azure AD open Id connect authentication scheme.
Now I have a requirement where I need to expose some APIs to an external system using client credentials flow. so I am not sure how to implement this in the current web app as it already has the open id connect authentication scheme.
Any idea?
Many thanks
You need to create two applications in Azure portal. At present, you already have an ASP.NET Core Web application, which represents the front-end application, so you need to create another application that represents the Web API, that is, the back-end application.
First, you need to expose the api of the back-end application protected by Azure, which can be configured according to the following process:
Azure portal>App registrations>Expose an API>Add a scope>Add a client application
Then you need to create the appRole of the back-end application, and then grant that role as an application permission to the client application.
Next, go to client application>API permissions>Add a permission>My APIs>your api application.
Finally, you need to obtain an access token using the client credential flow where no user is logged in:
Parse the token:
Finally, you can pass the token to the api application, and the api application will authenticate the client application by parsing the token.
Similar samples.
I had the same case and solved it this way:
Startup.cs
string[] scopes = new[]
{
"openid",
"profile",
"offline_access",
"email",
"scope_one",
"scope_two",
};
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://my-own-indentity.com";
options.Audience = "Any_Audience";
options.RequireHttpsMetadata = false;
options.SaveToken = true;
}) .AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = "https://Authority.com";
options.ClientId = "*****";
options.ClientSecret = "******";
options.CallbackPath = "/callback";
options.ResponseType = "code";
options.ResponseType = OidcConstants.ResponseTypes.Code;
options.RequireHttpsMetadata = false;
options.SaveTokens = true;
options.Scope.Clear();
options.GetClaimsFromUserInfoEndpoint = true;
foreach (string scope in scopes)
{
options.Scope.Add(scope);
}
});
To be authenticated by "AddJwtBearer" for controller:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[Route("[controller]")]
[ApiController]
public class ProjectController : ControllerBase
{
//your code
}
To be authenticated by "AddOpenIdConnect" for controller::
[Authorize(AuthenticationSchemes = "oidc")]
[ApiController]
[Route("api/[controller]")]
public class SomethingWithOpenIdController : Controller
{
//your code
}
If you found another solution feel free to share, otherwise I hope this helps you.
In Startup.cs
// Add services to the container.
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, (options) =>
{
options.TokenValidationParameters.ValidateIssuer = true;
options.MetadataAddress = "metadataaddress";
options.TokenValidationParameters.ValidAudience = "portal.com";
// or any specific option used for your application
});
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(
options =>
{
builder.Configuration.Bind("AzureAd", options);
// or any specific option used for your application
});
In Controllers:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme + "," + OpenIdConnectDefaults.AuthenticationScheme)]
[ApiController]
[Route("[controller]")]
public class SomeController : ControllerBase
This worked for me :)

ASP.NET Core 3.1 Use both OpenIDConnect and Custom Cookie Authentication

I have an existing application that makes use of Cookie Authentication, and would like to add the ability to authenticate users using Active Directory. The current application uses Cookie based authentication and custom authorisation - roles in a database.
I am adding bits from example located here:
Add sign-in with Microsoft to an ASP.NET Core web app
When I run the application I get an error:
System.InvalidOperationException: Scheme already exists: Cookies
What is the correct way to configure OpenIdConnect and Cookie Authentication.
// STEP 1 Basic Cookie Auth
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Auth";
options.AccessDeniedPath = "/Home/AccessDenied";
options.Cookie.IsEssential = true;
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromSeconds(day/2.0);
options.Cookie.HttpOnly = true; // not accessible via JavaScript
options.Cookie.Name = "login_token";
options.TicketDataFormat = new CustomJwtDataFormat(
SecurityAlgorithms.HmacSha256,
tokenValidationParameters);
});
// STEP 2 OpenID Connect Auth
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"), "OpenIdConnect", "Cookies", true);
I am not able to find any examples using both Cookie Authentication and OpenID Connect. Is this possible? Allowing users to login selectively using Active Directory authentication, or local authentication (details stored in local database).
After changing the "Cookie" name, get's rid of the error message,
but breaks the local authorisation, e.g.
When a valid Username and Password is given, I typically
authorise the login.
HttpContext.Response.Cookies.Append("login_token", token, GetCookieOptions());
Currently with OpenIDConnect configured User.Identity.IsAuthenticated
remains false.
According to the error messages, it tell you that you have multiple Scheme which named cookies.
According to the AddMicrosoftIdentityWebApp Method document, you could find the third parameter name is the cookieScheme.
The cookie-based scheme name to be used. By default it uses "Cookies".
But you have already set this name at above, so you should use other one. For example: "ADCookies".
Like below:
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"), "OpenIdConnect", "ADCookies", true);
The mixed approach is a minefield but the below is allowing use to Authenticate Users via IdentityServer4 using OIDC while authenticating the Application into AzureAD with Identity.Web to get tokens for Api calls.
services.AddAuthentication(options =>
{
options.DefaultScheme = "IS4Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("IS4Cookies")
.AddOpenIdConnect("oidc", "OpenID Connect", options =>
{
options.SignInScheme = "IS4Cookies";
// Get IdentityServer configuration from appsettings.json.
var config = Configuration.GetSection("IdentityServerOptions").Get<IdentityServerOptions>();
options.Authority = config.Authority;
options.RequireHttpsMetadata = false;
options.ClientId = config.ClientId;
options.ClientSecret = config.ClientSecret;
options.ResponseType = "code";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.ClaimActions.MapJsonKey("role", "role");
options.ClaimActions.MapJsonKey("role", System.Security.Claims.ClaimTypes.Role);
options.ClaimActions.MapJsonKey("email", "email");
options.ClaimActions.MapJsonKey("preferred_username", "preferred_username");
options.Events = new OpenIdConnectEvents
{
OnRemoteFailure = context =>
{
context.Response.Redirect("/");
context.HandleResponse();
return Task.FromResult(0);
}
};
})
.AddMicrosoftIdentityWebApp(Configuration, "AzureOptions")
.EnableTokenAcquisitionToCallDownstreamApi(new string[]{"sms.all" })
.AddInMemoryTokenCaches();
This is what I use and it works, you just need to specify the configureCookieAuthenticationOptions and set the name inside there and you should be good to go, also I had to use lax for SameSite or it would not work for me.
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(identityOptions =>
/* {identityOptions.ClientId ="";}, // if you want to specify the options manually instead of Configuration.GetSection() call*/
Configuration.GetSection("AzureAd"),
configureCookieAuthenticationOptions: authCookie => { // Setup SSO cookie
authCookie.Cookie.Name ="Your.Cookie.Name.Here";// change name to hide .net identifiers in name
authCookie.Cookie.HttpOnly = true;// make so client cannot alter cookie
authCookie.Cookie.SecurePolicy = CookieSecurePolicy.Always;// require https
authCookie.Cookie.SameSite = SameSiteMode.Lax;// from external resource
// verify options are valid or throw exception
authCookie.Validate();
}
);
You may or may not need all of the authCookie values here, but it should get you started in the right direction!
It's possible to mix two mechanisms.
I use MicrosoftIdentity authentication for access to administration web pages and cookies authentication for my APIs and SignalR hubs.
I use this in startup ConfigureServices
services
.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie("CookiesApiScheme", options =>
{
options.SlidingExpiration = true;
// There is no redirection to a login page for APIs and SignalR Hubs, I just made a call to /Api/Login/SignIn with credential
options.AccessDeniedPath = new PathString("/Api/Login/AccessDenied"); // Action who just returns an Unauthorized
})
.AddMicrosoftIdentityWebApp(Configuration); // By default scheme is "CookieAuthenticationDefaults.AuthenticationScheme"
And in API controller you can use something like this
[Route("api/[controller]")]
[ApiController]
[Authorize(Roles = Roles.ADMIN)]
[Authorize(AuthenticationSchemes = "CookiesApiScheme")]
public class DefaultController : ControllerBase
{
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
}
Based on this post: ASP.NET Core 2.0 AzureAD Authentication not working

Using OpenIdConnect and JwtBearer authentication together

My ASP.NET Core 2.2 web site uses OpenIdConnect to connect to an external OIDC provider for authentication. So we get a redirect to the provider, you log in, it returns back to the site. All of this is handled server side and works great.
Our web site uses javascript to communicate to the API backend, which is decorated with an [Authorize] attribute.
Now, we want to share our API with another application. I am having trouble understanding how to make it work with the existing OpenIdConnect provider. If I use Postman to make a call, I add a Bearer token but I am returned the log in screen for my OIDC provider. I understand this, it's set up for OIDC.
I have tried using AddJwtBearer with [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] set on my Controller and when I do that I can successfully pass in a Bearer token and call my API from Postman but I can't make a call from a javascript page on my site.
So the ultimate question is, how do I configure this site so that I can call an API from my site itself with OIDC and also call from an external app using a Bearer token?
The code looks like this:
public void ConfigureServices(IServiceCollection services)
{
var authbuilder = services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
});
authbuilder.AddCookie(p =>
{
p.SlidingExpiration = true;
p.Events.OnSigningIn = (context) =>
{
context.CookieOptions.Expires = DateTimeOffset.UtcNow.AddHours(14);
return Task.CompletedTask;
};
});
authbuilder.AddOpenIdConnect(options =>
{
options.Authority = Configuration["OpenIdConnectSettings:AuthorityUrl"];
options.ClientSecret = Configuration["OpenIdConnectSettings:ClientSecret"];
options.ClientId = Configuration["OpenIdConnectSettings:ClientId"];
}
options.SaveTokens = true;
options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
options.GetClaimsFromUserInfoEndpoint = true;
options.Events = new OpenIdConnectEvents
{
OnTokenValidated = context =>
{
// validation logic omitted
}
EDIT: Any external apps will also use the same OIDC provider. It's just a matter of getting my API Controllers to work from the web site itself and being called from another app that uses the same OIDC provider.
EDIT: I think I may have gotten it to work by adding this code:
services.AddAuthorization(options =>
{
var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
JwtBearerDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme);
defaultAuthorizationPolicyBuilder = defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});
if the other application is a service/application (not a human facing client) then that application needs to register as a client in your OIDC provider and then preferably use the client credentials flow, to get a token from your OIDC provider and pass it to the API.
The API only trusts access tokens from your OIDC provider (the token issuer), so the application needs to authenticate against the app and to get a proper access token.

IdentityServer4 and integration with signinmanager

We are working on an application that uses IdentityServer4, a web api endpoint, a web administration front end, and a mobile application. We have the web application set up like this
public void ConfigureServices(IServiceCollection services) {
services.AddMvc();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services
.AddAuthentication(options => {
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options => {
options.SignInScheme = "Cookies";
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("api1");
options.Scope.Add("offline_access");
});
}
This works and when we call into a controller that has an [Authorize] attribute on it, we get redirected, sign in, return to the calling site, and all is well.
Now we are trying to add authorization into the mix. I've seen the video where it is shown not to put authorization information into the token and I get that conceptually. But now I'd like to do something simple in the UI, like show or hide a link based on if the user is signed in or not. The default _LoginPartial page has something like this:
#inject SignInManager<User> SignInManager
#inject UserManager<User> UserManager
#if (SignInManager.IsSignedIn(User)) {
User is signed in
} else {
User is anonymous
}
In order to use SignInManager and UserManager, we'd have to set up the dependency service:
services.AddDbContext<Entities.ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<User, IdentityRole<long>>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
But when I do that, it messes up the authorization attribute on the home controller.
How do I, using IdentityServer4, go about testing whether a user is logged in or not and access my role storage in the web application? I must be missing something that is allowing us to externally authenticate and then turn around and figure out what roles / claims the user is able to be in / use.
Use things like User.Identity.IsAuthenticated and User.Identity.IsInRole, etc. instead. While you can back IdentityServer with ASP.NET Identity, it's important to realize that your MVC app is using IdentityServer, not Identity, for authentication and authorization. As such, things like SignInManager.IsSignedIn won't work.