I have to implement SSO with identitiserver4 (OAuth server).
I'm quite about authentication standards so before start working on it I make some research over the internet and I discovered that SSO is basically a social login where the provider is not a social network.
I read that identityserver4 implements oidc with which use the SSO.
In the Startup.cs file I added this:
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
})
.AddOpenIdConnect("oidc", options =>
{
//options.SignInScheme = "Cookies";
options.Authority = "<url-sso-server>";
options.ClientId = "<client-id>";
options.SaveTokens = true;
});
Is this settings enough? Whenever I will have to add a new SSO provider I just have to ask them for their url and clientId ? Of course, they will have to whitelist my redirect url /signin-oidc
Besides this I'm having trouble intercepting the redirect callback, I can't read any provider info:
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
return RedirectToAction(nameof(Login));
}
This always provide a null info.
Any advice?
Related
There are a few days I am reading the Duende Identity server (IdentityServer4) so I know about the different concepts and their usages such as Scopes, Resources, Client ...
The area I am confused about it is the clients. So I integrated the AspIdentity as an ApplicationUser in the IdentityServer (you can find the configs below in the code sections) but when I want to call the /connect/token which is a pre-defined endpoint from Duende, it needs to add ClientId and Secret but I want to use Username and the password of my registered user.
So the idea that comes to my mind is to Create a custom endpoint: after validating the user's credentials using SignInManager then I will find the Users client and then sign in to the Duende IdentityServer however I tried to do that but it is a bit inconvenience way to have an HTTP-call again to the same service to get the token of the User.
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(connectionString));
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
builder.Services.AddSwaggerGen();
builder.Services
.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
options.EmitStaticAudienceClaim = true;
})
.AddAspNetIdentity<ApplicationUser>()
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b =>
b.UseSqlite(connectionString, dbOpts => dbOpts.MigrationsAssembly(typeof(Program).Assembly.FullName));
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b =>
b.UseSqlite(connectionString, dbOpts => dbOpts.MigrationsAssembly(typeof(Program).Assembly.FullName));
options.EnableTokenCleanup = true;
options.RemoveConsumedTokens = true;
});
builder.Services.AddAuthentication();
if I can solve this issue in a convenient way so the other steps are very obvious and straightforward.
The clientID and secrets are meant to identify the application that wants to connect to IdentityServer and not the user; why can you not use clientID/secret for what it is intended for?
Also, the main purpose of OpenID connect is to not let the client application ever touch or see the user's username/password. That is why we delegate the authentication to IdentityServer.
I know this is absurd, and highly stupid to do, but how does your Startup.cs (for the client + Api Resource project) have to look, if you want to be both a Client towards the IdentityServer, and an ApiResource, using reference tokens?
The idea I had is the following: Require users to authenticate using OpenID Connect. Store credentials in cookies, but after that instead of relying on them, proceed to do what a normal ApiResource does with a reference token -> hit introspect endpoint, check if the token (grabbed from the cookie) is valid, if so -> allow access, if not -> revert to authentication.
Sadly I can't make the above behavior work at all. I'm not sure which schema goes where, especially the cookies one. If I set it as the default schema, authorization passes, but if the token is rejected, I still have access to the resources, because the API still looks at the cookie as a reference to the token, when in reality the token has already been revoked. (Introspect will return false)
I don't need any configuration pointers for the project hosting the Identity Server.
I think the way to do this is by using the Implicit grant type ,like this :
new Client
{
ClientId = "Client_implicit",
ClientSecrets = new [] { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.Implicit,
AllowedScopes = new [] {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
},
AllowAccessTokensViaBrowser = true,
RedirectUris = new [] { "...Api Adress .../signin-oidc" },
PostLogoutRedirectUris = { "...Api Adress ../signout-callback-oidc" },
}
On Api startup class :
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = "...IdentityServer Adress";
options.RequireHttpsMetadata = false;
options.ClientId = "Client_Implicit";
options.ClientSecret = "secret";
options.ResponseType = "id_token token";
options.Scope.Add("profile");
options.Scope.Add("openid");
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
});
And on your case you don't need to Api resource as your client is the Api resource also
for sign out create an action on your api for logout contain this :
await HttpContext.SignoutAsync("Cookie");
await HttpContext.SignoutAsync("OpenIdConnect");
so the senario for login will be like this :
the user will try to log in
the api will redirect the use to IdentityServer
IdentityServer will check user credentials and if success will
redirect to the api with (access_token)
the user now loggedIn
the senario for logout:
the user will call logout;
access_token will erase from cookie and from IdentitySever
I have two .net core 2.2 projects; the first one is an MVC project which is like presentation layer that user can login and the other Web API that handles DB operations. I want to handle login request in Web API and return the login result to MVC project by using MVC Core Identity. My DbContext is in the Web API project. Is there anyway to create identity cookie according to result of Web API request?
In this situation the authentication should be handled with an access token rather than a cookie.
For your Web API, implement Resource Owner Password Credentials grant type of OAuth2 protocol with the help of IdentityServer 4 library. At the end you should be able to get an access token from the API in exchange of login credentials.
For your MVC project, create a table to store token-sessid pairs, so when the MVC application gets an access token from the API during a session, it will save them in the table. For the subsequent requests the MVC app will get the token from the table (by using the sessid) and use it to access the Web API.
First in the startup class of your MVC on the server side
add->
services.AddAuthentication(options => {
options.DefaultScheme = "Cookies";
}).AddCookie("Cookies", options => {
options.Cookie.Name = "auth_cookie";
options.Cookie.SameSite = SameSiteMode.None;
options.Events = new CookieAuthenticationEvents
{
OnRedirectToLogin = redirectContext =>
{
redirectContext.HttpContext.Response.StatusCode = 401;
return Task.CompletedTask;
}
};
});
Then in your Login Controller of MVC
[HttpPost]
public async Task<IActionResult> Login(string username, string password)
{
if (!IsValidUsernameAndPasswod(username, password))
return BadRequest();
var user = GetUserFromUsername(username);
var claimsIdentity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, user.Username),
//...
}, "Cookies");
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
await Request.HttpContext.SignInAsync("Cookies", claimsPrincipal);
return NoContent();
}
Notice that we are referencing the “Cookies” authentication scheme we’ve defined in Startup.cs.
Afterwards on your web api->
CookieContainer cookieContainer = new CookieContainer();
HttpClientHandler handler = new HttpClientHandler
{
CookieContainer = cookieContainer
};
handler.CookieContainer = cookieContainer;
var client = new HttpClient(handler);
var loginResponse = await client.PostAsync("http://yourdomain.com/api/account/login?username=theUsername&password=thePassword", null);
if (!loginResponse.IsSuccessStatusCode){
//handle unsuccessful login
}
var authCookie = cookieContainer.GetCookies(new Uri("http://yourdomain.com")).Cast<Cookie>().Single(cookie => cookie.Name == "auth_cookie");
//Save authCookie.ToString() somewhere
//authCookie.ToString() -> auth_cookie=CfDJ8J0_eoL4pK5Hq8bJZ8e1XIXFsDk7xDzvER3g70....
This should help you achieve your Task. Of course change values based on your requirements, but thus code would be a good reference point.
Also add the required code to set cookies from your web api application.
Hope it helps!
I am not sure I’m fully up to speed with your problem statement. I am assuming that you’re after some sort of mechanism that enables a user to pass their identity all the way from web client to your DbContext.
I also assume you then don’t really authenticate users on the MVC app and basically proxy their requests onto WebAPI.
If so, you might want to consider crafting a JWT (preferably, signed) token as your WebAPI response and then storing it on the client (I guess cookie is a good enough mechanism).
Then MVC project will naturally get the token by virtue of Session State and all you will have to do would be to pass it along with every WebAPI request you make.
1. Only two services
If your system has only two services (front and back) you could use Cookies for all your authentication schemes in your front and consume your api for user validation.
Implement the login page in your web app and verify the users from your login action method (post) calling a backend endpoint (your api) where you can validate the credentials against your database. Note that you do not need to publish this endpoint in internet.
ConfigureServices:
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/auth/login";
options.LogoutPath = "/auth/logout";
});
AuthController:
[HttpPost]
public IActionResult Login([FromBody] LoginViewModel loginViewModel)
{
User user = authenticationService.ValidateUserCredentials(loginViewModel.Username, loginViewModel.Password);
if (user != null)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.Role, user.Role),
new Claim(ClaimTypes.Email, user.Email)
};
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync(principal);
return Redirect(loginViewModel.ReturnUrl);
}
ModelState.AddModelError("LoginError", "Invalid credentials");
return View(loginViewModel);
}
2. OAuth2 or OpenId Connect dedicated server
But if you want to implement your own authorization service or idenitity provider which can be used by all your applications (both front and back) I would recommend creating your own server with a standard like OAuth2 or OpenId. This service should be dedicated exclusively for this purpose.
If your services are net core you could use IdentityServer. It is a middleware that is certified by OpenIdConnect and is very complete and extensible. You have extensive documentation and it's easy to implement for both OAuth2 and OpenId. You could add your dbContext to use your user model.
Your Web app ConfigureServices:
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "https://myauthority.com";
options.ClientId = "client";
options.ClientSecret = "secret";
options.SaveTokens = true;
options.Scope.Clear();
options.Scope.Add("myapi");
// ...
}
Your Identity Provider ConfigureServices:
services.AddIdentityServer()
.AddInMemoryClients(Config.GetClients())
.AddInMemoryApiResources(Config.GetApis())
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddDeveloperSigningCredential();
In this way your fronts would request access to the scopes necessary to access the apis. The front would receive an access token with this scope (if this client is allowed for the requested scopes). These apis in turn could validate the access token with a validation middleware like the following:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://myauthority.com";
options.Audience = "myapi";
});
3. Custom remote handler
If you still prefer to implement something remote by yourself you can implement your custom RemoteAuthenticationHandler. This abstract class helps you to redirect to a remote login service (your api) and handles results on callback redirections with the authorization result in your web app. This result is used to populate the user ClaimsPrincipal and if you configure your web app authentication services in this way you could maintain the user session in a Cookie:
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "CustomScheme";
})
.AddCookie()
.AddRemoteScheme<CustomRemoteAuthenticationOptions, CustomRemoteAuthenticationHandler>("CustomScheme", "Custom", options =>
{
options.AuthorizationEndpoint = "https://myapi.com/authorize";
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.SaveTokens = true;
options.CallbackPath = "/mycallback";
});
You can see the remote handlers OAuthHandler or OpenIdConnectHandler as a guide to implement yours.
Implementing your own handler (and handler options) could be cumbersome and insecure, so you should to consider the first options.
I have configured the MVC client by adding the following lines.
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer();
The error message was, as (kind of) expected, 401 Unauthorized. So I added config for the bearer as suggested by Microsoft.
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(_ =>
{
_.Authority = "http://localhost:5000";
_.Audience = "http://localhost:5002";
});
In my solution, port 5000 hosts the IDS4 provider and port 5002 hosts the MVC application. At that point I got an error because I'm running strictly HTTP for the moment. The suggestion was to take the security down a notch by setting RequireHttpsMetadata to false, which I did as shown below.
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(_ =>
{
_.Authority = "http://localhost:5000";
_.Audience = "http://localhost:5002";
_.RequireHttpsMetadata = false;
})
To my disappointment, I'm back on getting 401 Unauthorized in my browser when requesting the page under action decorated by [Authorize].
I'm not sure how to diagnoze it further. I'm trying to compare my code to gazillion of examples but fail to see any significant difference. Also, many exmaples regard other version of Core, IDS or scheme. I need advise on where the smell might be coming from.
From IdentityServer4 samples you can see that they are using AddOpenIdConnect and not AddJwtBearer for the MVC Client sample. Your MVC client service registration should then look like below:
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.SaveTokens = true;
});
Lastly, make sure you have a client which has allowed scope to access your api resource and an appropriate grant type:
// OpenID Connect implicit flow client (MVC)
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.Implicit,
RedirectUris = { "http://localhost:5002/signin-oidc" },
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
}
}
AddOpenIdConnect basically preconfigures you the handler endpoints for callbacks from IDS4 to sign the user in and out as well as creates the appropriate ClaimsPrincipal.
Summary: Third party login breaks in iOS / OS 12!
We have a common login that works across multiple websites. This is working fine in Firefox, Chrome and Safari on Windows, macOS and iOS. But with iOS 12 and macOS 12, it seems cookies are no longer working from auth0 login window to our login API.
It has stopped working not just in Safari, but on iOS 12 also in Chrome and Firefox (it still works in Chrome on Mac OS 12). I suspect this has to do with Intelligent Tracking Prevention 2.0, but I'm struggling to find many technical details.
Our login flow is as follows:
User clicks login which sets window.location.href to login url on the universal (different) login domain.
This calls ChallengeAsync which sends user to auth0 domain for login.
User is then sent back to the login domain, but at this point the cookies from auth0 and session cookies set in the controller are missing.
I use the following in startup:
services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.Cookie.Path = "/";
options.SlidingExpiration = false;
})
.AddOpenIdConnect("Auth0", options => {
// Set the authority to your Auth0 domain
options.Authority = $"https://{Configuration["Auth0:Domain"]}";
// Configure the Auth0 Client ID and Client Secret
options.ClientId = Configuration["Auth0:ClientId"];
options.ClientSecret = Configuration["Auth0:ClientSecret"];
// Set response type to code
options.ResponseType = "code";
// Configure the scope
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.Scope.Add("offline_access");
options.CallbackPath = new PathString("/signin-auth0");
options.ClaimsIssuer = "Auth0";
options.SaveTokens = true;
options.Events = new OpenIdConnectEvents
{
OnRemoteFailure = context => {
<not relevant error redirects>
},
OnRedirectToIdentityProvider = context =>
{
context.ProtocolMessage.SetParameter("audience", $"{ Configuration["Auth0:ApiIdentifier"]}");
return Task.FromResult(0);
},
OnRedirectToIdentityProviderForSignOut = (context) =>
{
<not relevant logout handling>
}
};
});
In the login controller I have a login action which just sets a session value and calls ChallengeAsync to open the Auth0 login:
await HttpContext.ChallengeAsync("Auth0", new AuthenticationProperties() { IsPersistent = true, ExpiresUtc = DateTime.UtcNow.AddMinutes(Global.MAX_LOGIN_DURATION_MINUTES), RedirectUri = returnUri });
The "returnUri" parameter is the full path back to this same controller, but different action. It is when this action is hit that both cookies from the auth0 login (i.e. https://ourcompany.eu.auth0.com) and session data I set in the login action are missing in iOS 12.
Can I do it in some other way that will work on iOS 12? All help appreciated.
I have finally figured it out. The cookie set by default uses SameSiteMode.Lax. This has worked fine everywhere up until iOS 12, where it now needs to be set to SameSiteMode.None.
This is the modification I use that has got it working again:
.AddCookie(options =>
{
options.Cookie.Path = "/";
options.SlidingExpiration = false;
options.Cookie.SameSite = SameSiteMode.None;
options.Cookie.Expiration = TimeSpan.FromMinutes(Global.MAX_LOGIN_DURATION_MINUTES);
})