Identity Server 4 cookie setup not working - c#

I have been tasked with adding an mvc website to an existing web api project that uses Identity Server 4. Currently the web api authenticates Android clients using bearer tokens.
I would like the mvc site to use cookies which contain the bearer token.
I have been struggling with this for a few days now and I can make either the web client work, or the android client work, but not both. If I remove the default scheme, from the AddAuthentication(), the website will work as intended. This however causes the android client to not authorize users (HttpClient.User.Claims is null). If I do add the default scheme, the android client will work, but the website will not pick up the claims.
Any ideas on how to get this to work?
Image showing no claims being picked up
Here is my current setup:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) //TODO If I remove this default then the android client stops working (the web client will only work with this removed)
.AddCookie(options =>
{
options.LoginPath = new PathString("/Development/Account/Login");
options.AccessDeniedPath = new PathString("/Development/Account/Login");
options.LogoutPath = new PathString("/Development/Account/Logout");
options.ExpireTimeSpan = TimeSpan.FromDays(1);
})
.AddIdentityServerAuthentication(options =>
{
var configAuthority = container.GetInstance<IOptions<CoPilotConfig>>().Value?.IdentityAuthority;
options.Authority = configAuthority ?? $"http://localhost:{Constants.LOOPBACK_PORT}";
options.RequireHttpsMetadata = false;
options.ApiName = "CoPilotApi";
});
Here is the method used to login on the website portion:
[HttpPost("Login")]
[AllowAnonymous]
public async Task<ActionResult> Login(LoginModel model)
{
if (!ModelState.IsValid)
return View(model);
string deviceId = Guid.NewGuid().ToString("N");
string userString = deviceId + #"\" + _ctx.Company + #"\" + model.Username;
var accessToken = await GenerateTokenAsync(userString, model.Password);
if (accessToken != "invalid_grant")
{
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
JwtSecurityToken jwtToken = handler.ReadToken(accessToken) as JwtSecurityToken;
jwtToken.Claims.ToList();
var claimsIdentity = new ClaimsIdentity(jwtToken.Claims.ToList(), IdentityServerConstants.DefaultCookieAuthenticationScheme);
var authProperties = new AuthenticationProperties
{
AllowRefresh = true,
ExpiresUtc = DateTimeOffset.UtcNow.AddDays(1),
IssuedUtc = DateTime.UtcNow
};
try
{
await HttpContext.SignInAsync(IdentityServerConstants.DefaultCookieAuthenticationScheme, new ClaimsPrincipal(claimsIdentity), authProperties);
}
catch (Exception e){}
return RedirectToAction("Index", "Map");
}
else
{
model = new LoginModel();
model.Exception = "Invalid username or password";
return View("~/Views/Account/Login.cshtml", model);
}
}
private async Task<string> GenerateTokenAsync(string username, string password)
{
var clientList = IdentityServerConfig.GetClients();
Client client = clientList.FirstOrDefault();
string tokenUrl = $"http://" + HttpContext.Request.Host.ToString() + "/connect/token";
TokenClient tokenClient = new TokenClient(tokenUrl, client.ClientId, "secret");
var tokenResponse = await tokenClient.RequestResourceOwnerPasswordAsync(username, password, client.AllowedScopes.FirstOrDefault());
if (tokenResponse.IsError)
return "invalid_grant";
return tokenResponse.AccessToken;
}
Any ideas on how to fix this or another way of handling this issue without having to modify the Android client?

I figured it out. To anyone trying this you add a middleware to determine which scheme to use for the incoming request:
public class AuthSchemeMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext context, Func<Task> next)
{
var scheme = context.Request.Cookies.ContainsKey("idsrv.session")
? "Cookies" : "Bearer";
var result = await context.AuthenticateAsync(scheme);
if (result.Succeeded)
{
context.User = result.Principal;
}
await next();
}
}
You will call the new middleware here:
// use Authentication from IdentityServer4
app.UseAuthentication();
// Serve static files
app.UseDefaultFiles();
app.UseStaticFiles(new StaticFileOptions()
{
ContentTypeProvider = ContentTypeProviderFactory.GetContentTypeProvider()
});
app.UseMiddlewareFrom<AuthSchemeMiddleware>(container);
app.UseMvcWithDefaultRoute();
Authentication should be added like this:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "Cookies";
})
.AddCookie("Cookies", options =>
{
options.LoginPath = new PathString("/Development/Account/Login");
options.AccessDeniedPath = new PathString("/Development/Account/Login");
options.LogoutPath = new PathString("/Development/Account/Logout");
options.ExpireTimeSpan = TimeSpan.FromDays(1);
});
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
var configAuthority = container.GetInstance<IOptions<CoPilotConfig>>().Value?.IdentityAuthority;
options.Authority = configAuthority ?? $"http://localhost:{Constants.LOOPBACK_PORT}";
options.RequireHttpsMetadata = false;
options.ApiName = "CoPilotApi";
});

Related

How to get [Authorize] attribute work in .NET Core 6 Web API?

I am a noob, trying to do JWT the simplest possible way on a .NET Core 6 Web API project, but I can't even get it to work.
Requirement: You need to be logged in to call the GetProductList API.
(I am testing this on Swagger that comes with the project)
FYI, my Login controller: (working as intended)
[HttpPost("login")]
public async Task<ActionResult> Login(LoginDto request)
{
var user = GetUserFromRequest(request);
if (user == null)
return BadRequest("Invalid credentials.");
string jwt = CreateJwtToken(user.Id.ToString());
Response.Cookies.Append(COOKIE_JWT, jwt, _cookieOptions);
return Ok();
}
[HttpGet("user")]
public IActionResult GetUser()
{
try
{
var jwt = Request.Cookies[COOKIE_JWT];
var userId = VerifyJwtAndGetUserId(jwt);
return Ok(GetUserById(userId));
}
catch(Exception ex)
{
return Unauthorized();
}
}
public static string CreateJwtToken(string userId)
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JWT_KEY));
var cred = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature);
var token = new JwtSecurityToken(
issuer: userId,
expires: DateTime.Now.AddDays(365),
signingCredentials: cred
);
var jwt = new JwtSecurityTokenHandler().WriteToken(token);
return jwt;
}
public static string VerifyJwtAndGetUserId(string jwt)
{
var tokenHandler = new JwtSecurityTokenHandler();
tokenHandler.ValidateToken(jwt, new TokenValidationParameters {
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JWT_KEY)),
ValidateIssuerSigningKey = true,
ValidateIssuer = false,
ValidateAudience = false
}, out SecurityToken validatedToken);
string userId = validatedToken.Issuer;
return userId;
}
The question is, how can I make the [Authorize] attribute work?
[HttpGet("list")]
//[Authorize]
public async Task<ActionResult<List<Product>>> GetProductList()
{
return Ok(GetProducts());
}
The above works, but adding [Authorize] attribute gives a 401 with the following header: (while GetUser above is fine)
content-length: 0
date: Mon,13 Jun 2022 23:27:32 GMT
server: Kestrel
www-authenticate: Bearer
This is what's in my Program.cs: (maybe this is wrong?)
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters { // similar to the one in controller
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JWT_KEY)),
ValidateIssuerSigningKey = true,
ValidateIssuer = false,
ValidateAudience = false
};
options.Events = new JwtBearerEvents { // https://spin.atomicobject.com/2020/07/25/net-core-jwt-cookie-authentication/
OnMessageReceived = ctx => {
ctx.Token = ctx.Request.Cookies["jwt"];
return Task.CompletedTask;
}
};
});
SOLUTION:
Move app.UseAuthentication(); above app.UserAuthorization();.
Summary
FrontEnd
According to JWT Introduction as below
Whenever the user wants to access a protected route or resource, the
user agent should send the JWT, typically in the Authorization header
using the Bearer schema. The content of the header should look like
the following:
Authorization: Bearer [token]
So you need to add a header Authorization: Bearer <token> in your request on frontEnd.
a simple example at frontEnd. In this example the yourApiUrl should be your list api route.
const token = getCookieValue(`COOKIE_JWT`);
fetch(yourApiUrl, {
headers: {
"Authorization": `Bearer ${token}`,
}
})
getCookieValue function
const getCookieValue(name) =>{
document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)')?.pop() || ''
}
then your request will have the authorization header with a prefix [Bearer].
a picture from my browser's dev tool.
and then the [Authorize] attribute should work
BackEnd
Maybe it is not the problem at frontEnd, then you can check the backEnd's code.
Some code example at backEnd .NET 6 WebApi about generate or examine the JWT token
GenerateToken function
Use to generate token when sign-in controller
public string GenerateToken(TokenModel tokenModel, int
expireMinutes = 30)
{
var issuer = this._configuration.GetValue<string> ("JwtSettings:Issuer");
var signKey = this._configuration.GetValue<string>
("JwtSettings:SignKey");
// Configuring "Claims" to your JWT Token
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Iss, issuer),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), // JWT ID
new Claim(JwtRegisteredClaimNames.Sub, tokenModel.EmployeeNo), // User.Identity.Name
new Claim(JwtRegisteredClaimNames.NameId, tokenModel.EmployeeNo),
new Claim(JwtRegisteredClaimNames.Name, tokenModel.EmployeeName),
};
for (int i = 0; i < tokenModel.Roles.Length; i++)
{
claims.Add(new Claim("roles", tokenModel.Roles[i]));
}
var userClaimsIdentity = new ClaimsIdentity(claims);
// Create a SymmetricSecurityKey for JWT Token signatures
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signKey));
// HmacSha256 MUST be larger than 128 bits, so the key can't be too short. At least 16 and more characters.
var signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature);
// Create SecurityTokenDescriptor
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = userClaimsIdentity,
NotBefore = DateTime.Now, // Default is DateTime.Now
IssuedAt = DateTime.Now, // Default is DateTime.Now
Expires = DateTime.Now.AddMinutes(expireMinutes),
SigningCredentials = signingCredentials
};
// Generate a JWT securityToken, than get the serialized Token result (string)
var tokenHandler = new JwtSecurityTokenHandler();
var securityToken = tokenHandler.CreateToken(tokenDescriptor);
var serializeToken = tokenHandler.WriteToken(securityToken);
return serializeToken;
}
Program.cs
.NET6 WebApi's setting in Program.cs about examine Jwt token.
📌 [NOTICE]: the method builder.Services.AddAuthorization(); MUST below the builder.Services.AddAuthentication method, or
will result error.
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) //check the HTTP Header's Authorization has the JWT Bearer Token
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
// varify Issuer
ValidateIssuer = true,
ValidIssuer = builder.Configuration.GetValue<string>("JwtSettings:Issuer"),
// 📌 Important!!! audience need to be set to false
// Because the default is true.
// So if you didn't set this prop when generate token, then the api will always return a check token error
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey =
new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration.GetValue<string>("JwtSettings:SignKey")))
};
});
builder.Services.AddAuthorization();
Easy Test by thunder client of VsCode
Use the thumder client VsCode to test the api route.
Use the swagger's login controller to get the JWTToken.
Then paste the token in thunder client, and add the prefix Bearer
a simple image of thunder client

Multiple Authentication Schemes JWT + Azure AD

I'm trying to get dual authentication schemes working with Azure Ad and regular jwt tokens, the issue i'm having is, I can access both controller functions with just the JWT token, regardless of scheme. I tried setting up my configuration from this post but it didn't work (neither does my current configuration obviously):
Authenticating tokens from multiple sources (e.g Cognito and Azure)
[HttpGet]
[Authorize(AuthenticationSchemes = "TestBearer")]
[Route("Test1")]
public async Task Test()
{
}
[HttpGet]
[Authorize(AuthenticationSchemes = "AzureAd")]
[Route("Test2")]
public async Task Test2()
{
}
Startup config
services.AddAuthentication(options =>
{
options.DefaultScheme = "AzureAd";
})
.AddJwtBearer("TestBearer", x =>
{
x.RequireHttpsMetadata = false;
x.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Guid.Parse(jwtConfig.SecretKey).ToByteArray())
};
})
.AddMicrosoftIdentityWebApi(x =>
{
x.Audience = config.Audience;
x.Authority = config.Authority;
x.RequireHttpsMetadata = false;
},
x =>
{
x.Instance = config.Instance;
x.ClientId = config.ClientId;
x.TenantId = config.TenantId;
x.Authority = config.Authority;
x.RequireHttpsMetadata = false;
}, "AzureAd");
services.AddAuthorization(options =>
{
var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
"TestBearer", "AzureAd");
defaultAuthorizationPolicyBuilder =
defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});
var scopes = new string[] { "https://graph.microsoft.com/.default" };
IConfidentialClientApplication confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create(config.ClientId)
.WithTenantId(config.TenantId)
.WithClientSecret(config.ClientSecret)
.Build();
//Build the Microsoft Graph client.As the authentication provider, set an async lambda
// which uses the MSAL client to obtain an app-only access token to Microsoft Graph,
// and inserts this access token in the Authorization header of each API request.
var authenticationProvider = (new DelegateAuthenticationProvider(async (requestMessage) =>
{
// Retrieve an access token for Microsoft Graph (gets a fresh token if needed).
var authResult = await confidentialClientApplication
.AcquireTokenForClient(scopes)
.ExecuteAsync();
//// Add the access token in the Authorization header of the API request.
requestMessage.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
}));
services.AddScoped(_ => new GraphServiceClient(authenticationProvider));
my jwt token generation
private string GenerateToken(TestEntity entity)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Guid.Parse(_jwtSettings.SecretKey).ToByteArray();
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]{
new Claim(ClaimTypes.NameIdentifier, entity.Id.ToString()),
new Claim(ClaimTypes.MobilePhone, entity.Phone.ToString()),
new Claim(ClaimTypes.Email, entity.Email)
}),
Expires = DateTime.UtcNow.AddHours(_jwtSettings.ExpiryTimeInHours),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature),
Audience = "https://localhost:5001",
Issuer = _jwtSettings.Issuer,
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
So I ended up seperating the policies (leaving azure AD as my default policy) and creating an extra combined one in cases I want one of multiple policies to work. All seems to work correctly now.
services.AddAuthorization(options =>
{
options.AddPolicy("Test", policy =>
{
policy.AuthenticationSchemes.Add("TestBearer");
policy.RequireAuthenticatedUser();
});
options.AddPolicy("AzureOrTest", policy =>
{
policy.AuthenticationSchemes.Add("TestBearer");
policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
});
options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build();
});

ASP.NET Core 3.1 Web API Role based authorization not working

I couldn't understand why I always get 401 unauthorized where the user I logged in has a role of SuperAdmin. I tried looking at other solutions and projects and they seem identical to the code I have still does not work. I use Postman to test the API and in Authorization tab bearer token I pasted the token of the user I logged in and make a request on this API.
//API
[Route("create")]
[Authorize(Roles = "SuperAdmin")]
public async Task<IActionResult> RegisterUserAsync([FromBody] Request request)
{
return something;
}
//StartUp.cs
private void ConfigureAuth(IServiceCollection services)
{
services.AddIdentity<UserEntity, RoleEntity>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders()
.AddRoles<RoleEntity>();
}
var key = Encoding.ASCII.GetBytes(jwtSettings.Secret);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
//JWT
public string GenerateToken(UserEntity userEntity, IList<string> roles)
{
var token = string.Empty;
var tokenHandler = new JwtSecurityTokenHandler();
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this.jwtOptions.GetJwtOptions().Secret));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature);
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Name, userEntity.UserName),
new Claim(ClaimTypes.GivenName, userEntity.FirstName),
new Claim(ClaimTypes.Surname, userEntity.LastName),
new Claim(ClaimTypes.NameIdentifier, userEntity.Id.ToString()),
new Claim(ClaimTypes.Role, roles.FirstOrDefault()) //SuperAdmin
};
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddMinutes(this.jwtOptions.GetJwtOptions().ExpiresInMinutes),
SigningCredentials = credentials
};
token = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor));
return token;
}
You need to add app.UseAuthentication() before app.UseAuthorization() , the authentication middleware will handle the JWT bearer authentication , validate and decode token , at last write to user's principle.

I can t find why my bearer token is not valid, its returns 401

I have created role-based authentication. Token is generated succesfully.
In my vue app, after login i m adding token to localStorage and then, im sending it in header by use of axios in format:
Authorization: "Bearer"
I m getting unauthorize everytime i m trying to do GET on items.
In postman is this same.
I m using IIS to host my app localy, and as frontend i m using Vue.js
I have already tried:
- authorization and Authorization in header
- changing http to https and vice versa.
Startup ConfigureServices:
#region JWT
var key = Encoding.ASCII.GetBytes(Configuration.GetValue<string>("SecretKey"));
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
#endregion
Startup Configure:
app.UseAuthentication();
My method to generate token based on roles:
public User Authenticate(string login, string password)
{
var user = Context.Users.SingleOrDefault(p => p.Login == login && p.Password == password);
if (user == null)
return null;
var tokenHandler = new JwtSecurityTokenHandler();
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, user.Id.ToString()),
new Claim(ClaimTypes.Role, user.Role)
}),
Expires = DateTime.UtcNow.AddHours(1),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(Key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
user.Token = tokenHandler.WriteToken(token);
user.Password = null;
return user;
}
And this is my Endpoint (in case if something is wrong here)
[Route("api/[controller]")]
[EnableCors("MyPolicy")]
[Authorize]
[ApiController]
public class ItemsController : ControllerBase
{
public ItemsController()
{
}
[HttpGet]
[Authorize(Roles = "Admin")]
public ActionResult<IEnumerable<Item>> Get()
{
var items = new ItemsService();
return items
.GetItems();
}

Authentication Settings for Amazon and Evernote

If you refer to https://learn.microsoft.com/en-us/aspnet/core/migration/1x-to-2x/identity-2x?view=aspnetcore-2.2 you can see you can configure OpenID Connect (OIDC) authentication for various providers as below:
Facebook
services.AddAuthentication()
.AddFacebook(options =>
{
options.AppId = Configuration["auth:facebook:appid"];
options.AppSecret = Configuration["auth:facebook:appsecret"];
});
Google
services.AddAuthentication()
.AddGoogle(options =>
{
options.ClientId = Configuration["auth:google:clientid"];
options.ClientSecret = Configuration["auth:google:clientsecret"];
});
Microsoft
services.AddAuthentication()
.AddMicrosoftAccount(options =>
{
options.ClientId = Configuration["auth:microsoft:clientid"];
options.ClientSecret = Configuration["auth:microsoft:clientsecret"];
});
My question is does anybody have the settings I would need to provide to Support Amazon and Evernote OIDC?
You can find Login with Amazon reference here
Amazon still does not support OIDC, but supports OAuth. However the default OAuthHandler for dotnet does not provide UserInfoEndpoint handling. That's why you have to either implement the call to UserInfoEndpoint (can grab it from oidc) or hack the OIDC to make it thinking that it has id_token whenever it has not. I've passed the second route. Little bit dirty trick, but I've got my user identified.
.AddOpenIdConnect("lwa", "LoginWithAmazon", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.Authority = "https://www.amazon.com/";
options.ClientId = "amzn1.application-oa2-client.xxxxxxxxxxxxxx";
options.ClientSecret = "xxxxxxxxxxxxxxxxx";
options.ResponseType = "code";
options.ResponseMode = "query";
options.SaveTokens = true;
options.CallbackPath = "/signin-amazon";
options.SignedOutCallbackPath = "/signout-callback-amazon";
options.RemoteSignOutPath = "/signout-amazon";
options.Scope.Clear();
options.Scope.Add("profile");
options.GetClaimsFromUserInfoEndpoint = true;
var rsa = RSA.Create();
var key = new RsaSecurityKey(rsa){KeyId = "1"};
var jwtClaims = new List<Claim>
{
new Claim(JwtClaimTypes.IssuedAt, "now"),
new Claim(JwtClaimTypes.JwtId, Guid.NewGuid().ToString()),
new Claim(JwtClaimTypes.Subject, Guid.NewGuid().ToString())
};
var jwt = new JwtSecurityToken(
"issuer",
"audience",
jwtClaims,
DateTime.UtcNow,
DateTime.UtcNow.AddHours(1),
new SigningCredentials(key, "RS256"));
var handler = new JwtSecurityTokenHandler();
handler.OutboundClaimTypeMap.Clear();
var token = handler.WriteToken(jwt);
options.Configuration = new OpenIdConnectConfiguration
{
AuthorizationEndpoint = "https://www.amazon.com/ap/oa",
TokenEndpoint = "https://api.amazon.com/auth/o2/token",
UserInfoEndpoint = "https://api.amazon.com/user/profile"
};
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateTokenReplay = false,
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = false,
IssuerSigningKey = key
};
AuthorizationCodeReceivedContext hook = null;
options.Events = new OpenIdConnectEvents
{
OnAuthenticationFailed = async context =>
{
//context.SkipHandler();
},
OnAuthorizationCodeReceived = async context => { hook = context; },
OnTokenResponseReceived = async context =>
{
context.TokenEndpointResponse.IdToken = token;
hook.TokenEndpointResponse = context.TokenEndpointResponse;
},
OnUserInformationReceived = async context =>
{
var user = context.User;
var claims = new[]
{
new Claim(JwtClaimTypes.Subject, user["user_id"].ToString()),
new Claim(JwtClaimTypes.Email, user["email"].ToString()),
new Claim(JwtClaimTypes.Name, user["name"].ToString())
};
context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims));
context.Success();
}
};
})
Unfortunately, neither Login with Amazon nor Evernote supports Open ID Connect. Other mentioned services do, which can be verified by visiting appropriate configuration site of each of them: Google, Microsoft.
There are ofc others that are not preconfigured in .Net and can be used with it:
Salesforce
As you probably noticed usually the configuration for Open ID Connect is stored on a site with "/.well-known/openid-configuration" suffix. This is called OpenID Connect metadata document and it contains most of the information required for an app to do sign-in. This includes information such as the URLs to use and the location of the service's public signing keys.
And now lets go for .Net configuration for custom Open ID Connect provider (I will use Salesforce as it supports Open ID):
services.AddAuthentication()
.AddFacebook(options =>
{
options.AppId = Configuration["auth:facebook:appid"];
options.AppSecret = Configuration["auth:facebook:appsecret"];
})
.AddOpenIdConnect("OpenIdConnectSalesforce", "Salesforce", options =>
{
options.Authority = "https://login.salesforce.com";
options.ClientId = Configuration["auth:salesforce:appid"];
options.ClientSecret = Configuration["auth:salesforce:appsecret"];
options.ResponseType = "code";
});
And after launching web app we can see additional button to log in using Salesforce:
As for Evernote and Amazon you could use their SDKs and APIs to implement their log in methods respectively. I do believe that they support OAuth.
Extended the solution by #d-f to use OAuth handler.
.AddOAuth("lwa-oauth", "OauthLoginWithAmazon", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.ClientId = "amzn1.application-oa2-client.zzzzzzzzzzzz";
options.ClientSecret = "4c0630b4166c901519a730835ezzzzzzzzzzzzzzzz";
options.SaveTokens = true;
options.CallbackPath = "/signin-amazon";
options.Scope.Clear();
options.Scope.Add("profile");
options.AuthorizationEndpoint = "https://www.amazon.com/ap/oa";
options.TokenEndpoint = "https://api.amazon.com/auth/o2/token";
options.UserInformationEndpoint = "https://api.amazon.com/user/profile";
options.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
var accessToken = context.AccessToken;
HttpResponseMessage responseMessage =
await context.Backchannel.SendAsync(
new HttpRequestMessage(HttpMethod.Get, options.UserInformationEndpoint)
{
Headers =
{
Authorization = new AuthenticationHeaderValue("Bearer", accessToken)
}
});
responseMessage.EnsureSuccessStatusCode();
string userInfoResponse = await responseMessage.Content.ReadAsStringAsync();
var user = JObject.Parse(userInfoResponse);
var claims = new[]
{
new Claim(JwtClaimTypes.Subject, user["user_id"].ToString()),
new Claim(JwtClaimTypes.Email, user["email"].ToString()),
new Claim(JwtClaimTypes.Name, user["name"].ToString())
};
context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims));
context.Success();
}
};
})

Categories

Resources