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();
}
};
})
Related
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();
});
I have a .NET 5 project. I am trying to generate a JWT within. This line is giving me an error deployed to an IIS web server: JwtSecurityTokenHandler().WriteToken(token)
The encryption algorithm 'System.String' requires a key size of at least 'System.Int32' bits.
Key 'Microsoft.IdentityModel.Tokens.SymmetricSecurityKey', is of size: 'System.Int32'. (Parameter 'key')
There is commented code I have put in there to test that the values going into the token generation are OK, and the key is 16 characters long (I have tested with a lot more and still fails).
This works fine in my local environment.
Anyone know why this might be?
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.Id),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
var key = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_jwtConfig.Value.SecretKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(_jwtConfig.Value.Issuer,
_jwtConfig.Value.Audience,
claims,
expires: DateTime.UtcNow.AddDays(30),
signingCredentials: creds);
//return new JsonResult(new Dictionary<string, object>
// {
// { "secretkey", _jwtConfig.Value.SecretKey },
// { "creds", creds.ToString() },
// { "issuer", _jwtConfig.Value.Issuer },
// { "audience", _jwtConfig.Value.Audience },
// { "token", token.ToString() }
// });
return new JsonResult(new Dictionary<string, object>
{
{ "access_token", new JwtSecurityTokenHandler().WriteToken(token) },
});
Relevant section from Startup.cs:
services.Configure<JWTSettings>(Configuration.GetSection("JWTSettings"));
services.AddAuthentication()
.AddCookie()
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.IncludeErrorDetails = true;
var secretKey = Configuration.GetSection("JWTSettings:SecretKey").Value;
var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey));
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = Configuration.GetSection("JWTSettings:Issuer").Value,
ValidateAudience = true,
ValidAudience = Configuration.GetSection("JWTSettings:Audience").Value,
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey,
};
})
It seems you have error IDX10653.
But your exception message do not display real information. Please review this article for details. Could you add the code IdentityModelEventSource.ShowPII = true; and provide real exception message?
I think you will see real exception message and you can understand what happened.
Highly likely your SecretKey is less then 128 bits. In this case just try to use more long value for parameter JWTSettings:SecretKey in your configuration file. So similar scenario was described here.
It does not appear to be coding issue, more something on the .NET Configuration of the web host. Deploying the application as "Framework Dependent" results in the error, however "Self Contained" the code itself works.
Alright so I might have the solution but I'm not so sure so let me know if this works:
I've configured my JWT token directly in my Startup.cs like so:
Startup.cs
public static void AddIdentityServices(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<JwtSettings>(configuration.GetSection("JwtSettings"));
// Identity stuff
services.AddTransient<IAuthenticationService, AuthenticationService>();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "JwtBearer";
options.DefaultChallengeScheme = "JwtBearer";
})
.AddJwtBearer("JwtBearer", o =>
{
o.RequireHttpsMetadata = false;
o.SaveToken = true;
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidIssuer = configuration["JwtSettings:Issuer"],
ValidAudience = configuration["JwtSettings:Audience"],
ClockSkew = TimeSpan.FromMinutes(5),
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(configuration["JwtSettings:Key"]))
};
o.Events = new JwtBearerEvents()
{
OnAuthenticationFailed = c =>
{
c.NoResult();
c.Response.StatusCode = 500;
c.Response.ContentType = "text/plain";
return c.Response.WriteAsync($"{c.Exception.ToString()} JwtBearerEvents in IdentityServiceRegistration");
},
OnChallenge = context =>
{
context.HandleResponse();
context.Response.StatusCode = 401;
context.Response.ContentType = "application/json";
var result = JsonConvert.SerializeObject("401 Not authorized");
return context.Response.WriteAsync(result);
},
OnForbidden = context =>
{
context.Response.StatusCode = 403;
context.Response.ContentType = "application/json";
var result = JsonConvert.SerializeObject("403 Not authorized");
return context.Response.WriteAsync(result);
},
};
});
}
And here's my token generation method used by CreateUserAsync and other methods:
private async Task<JwtSecurityToken> GenerateToken(ApplicationUser user)
{
var claims = new[]
{
new Claim(ClaimTypes.Name, user.UserName),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.Email, user.Email),
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(JwtRegisteredClaimNames.Nbf,
new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds().ToString()),
new Claim(JwtRegisteredClaimNames.Exp,
new DateTimeOffset(DateTime.Now.AddYears(10))
.ToUnixTimeSeconds().ToString())
}
.Union(userClaims);
var jwtSecurityToken = new JwtSecurityToken(
new JwtHeader(
new SigningCredentials(
new SymmetricSecurityKey(Encoding.UTF8.GetBytes("//secret key of 64 characters of capital letters and numbers")),
SecurityAlgorithms.HmacSha256)),
new JwtPayload(claims));
return jwtSecurityToken;
}
You should be able to generate a key from a website like this: SHA256 Generator
Let me know if it helps.
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.
Using .Net Core 3.1 WebApi with Jwt authentication seems to work fine unless we try to use ValidateIssuer and Validate Audience.
When we set these properties to true, we get an Unauthorized Http Status code.
We get the values for Audience and Issuer from our app settings, so we know they are the same.
Following is the code from our startup.cs:
//
// Configure JWT authentication from the 'jwtIssuerOptions' values in the appsettings.json file
//
Models.JwtIssuerOptions jwtSettings = _appConfiguration.GetSection("jwtIssuerOptions").Get<Models.JwtIssuerOptions>();
var keyBytes = Encoding.UTF8.GetBytes(jwtSettings.JwtSecret);
SymmetricSecurityKey symmetricSecurityKey = new SymmetricSecurityKey(keyBytes);
services.AddAuthentication(a =>
{
a.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
a.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(b =>
{
b.RequireHttpsMetadata = false;
b.SaveToken = true;
b.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidAudience = jwtSettings.Audience ,
ValidIssuer = jwtSettings.Issuer ,
ValidateIssuerSigningKey = true,
IssuerSigningKey = symmetricSecurityKey,
TokenDecryptionKey = symmetricSecurityKey,
ValidateIssuer = true,
ValidateAudience = true
};
});
Following is the code from our Auth Helper that creates the Jwt:
private void CreateTheJWT(EndUserCredentials user)
{
var keyBytes = Encoding.UTF8.GetBytes(_jwtIssuerOptions.JwtSecret);
var symmetricSecurityKey = new SymmetricSecurityKey(keyBytes);
var signingCredentials = new SigningCredentials(symmetricSecurityKey, SecurityAlgorithms.HmacSha256Signature);
var cryptoKey = new EncryptingCredentials(symmetricSecurityKey, JwtConstants.DirectKeyUseAlg, SecurityAlgorithms.Aes256CbcHmacSha512);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[] {
new Claim(ClaimTypes.Name, user.Name),
new Claim(ClaimTypes.Email, user.Name),
new Claim("EndUser", System.Text.Json.JsonSerializer.Serialize( user)),
}),
Expires = DateTime.UtcNow.AddMinutes(_jwtIssuerOptions.TimeoutMinutes),
Audience = _jwtIssuerOptions.Issuer,
Issuer = _jwtIssuerOptions.Audience,
NotBefore = DateTime.UtcNow.AddMinutes(-2),
IssuedAt = DateTime.UtcNow.AddMinutes(-1),
SigningCredentials = signingCredentials,
EncryptingCredentials = cryptoKey
};
foreach (string role in user.Roles)
{
tokenDescriptor.Subject.AddClaim(new Claim(ClaimTypes.Role, role));
}
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
user.Token = tokenHandler.WriteToken(token);
}
When we set the offending properties to "false", everything works. Any help is appreciated!
it seems to me that your issuer and audiance are swapped. can you reassign in CreateTheJWT function?
Audience = _jwtIssuerOptions.Audience,
Issuer = _jwtIssuerOptions.Issuer,
If you are here like me and you didn't swap your issuer and audience.
Ensure you have this in your Startup.ConfigureServices.
app.UseAuthentication();
app.UseAuthorization();
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";
});