.NET Framework C# OIDC Add PKCE to Amazon AWS Cognito - c#

I have three applications that all use different .NET methodologies (I don't know a better word off-hand). I have a .NET Core 3.1 Web App, a .NET Framework 4.8 MVC Web App, and a .NET Framework 4.6 Web Forms Application. All written in C#.
All three require that I use PKCE (Proof Key for Code Exchange). All three applications use Amazon AWS Cognito for Authentication and Authorization. Everything works well except that the two non-.NET Core apps don't use PKCE and I need them to.
Okay, so my main thinking here is that there might possibly be some "easy breezy" way I can add PKCE support to the .NET Framework applications via the Startup.Auth.cs file or some such were we define the OIDC Auth.
What I've done:
I've been able to find some online articles on how to add PKCE to a site, but they're all manual mechanisms of adding the necessary bits to the query string.
How to secure the Cognito login flow with a state nonce and PKCE
Authorize endpoint
ASP.NET Core using Proof Key for Code Exchange (PKCE)
The .NET Core article was particularly useful for my identification of how my .NET Core app works, but it doesn't help with the other two apps. Overall, the process seems to be broken down into the following steps:
Create a Code_Verifier Call it VERIFIER.
SHA256 that Code_Verifier and base 64 encode it. Call it the CHALLENGE.
Add this to the query string we send to the "authorize" endpoint along with a code_challenge_method which must, for Cognito, be set to S256.
Add to the query string that we send to the "token" endpoint, the Code_Verifier with the value VERIFIER.
In essence, we could manually add the following to the query string that calls the "authorize" endpoint: "&code_challenge=CHALLENGE&code_challenge_method=S256".
And to the query string that calls the "token" endpoint: "&code_verifier=VERIFIER". (I'm pretty sure this token endpoint is a POST.)
I think that's it. So, I could add this stuff manually, somehow, to these calls. However, I would prefer to let the .NET Framework magically do the work for me.
Hence, my overall question. Is there a way for me to modify the .NET code I have to have PKCE added by modifying my code without manually adding it.
I've searched the web, the Microsoft site, and I've fiddled around with the actual code, but I'm not finding any mechanisms for adding PKCE in these methods.
My Examples from my code:
.NET Framework Web Forms App:
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(DefaultAuthenticationTypes.ApplicationCookie);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
CookieManager = new SystemWebCookieManager()
});
ConfigureIdentityProviders(app, DefaultAuthenticationTypes.ApplicationCookie);
}
private void ConfigureIdentityProviders(IAppBuilder app, string signInAsType)
{
var saveTokens = true;
var validateIssuer = true;
var saveTokensValue = ConfigurationManager.AppSettings["Cognito.SaveTokens"];
if (!string.IsNullOrEmpty(saveTokensValue))
{
saveTokens = bool.TryParse(saveTokensValue, out var outResult) && outResult;
}
var validateIssuerValue = ConfigurationManager.AppSettings["Cognito.TokenValidationParameters.ValidateIssuer"];
if (!string.IsNullOrEmpty(validateIssuerValue))
{
validateIssuer = bool.TryParse(validateIssuerValue, out var outResult) && outResult;
}
app.UseCustomOidcAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = ConfigurationManager.AppSettings["Cognito.ClientId"],
ResponseType = ConfigurationManager.AppSettings["Cognito.ResponseType"],
Authority = ConfigurationManager.AppSettings["Cognito.Authority"],
MetadataAddress = ConfigurationManager.AppSettings["Cognito.MetadataAddress"],
ClientSecret = ConfigurationManager.AppSettings["Cognito.ClientSecret"],
RedirectUri = ConfigurationManager.AppSettings["Cognito.RedirectUri"],
SaveTokens = saveTokens,
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = ConfigurationManager.AppSettings["Cognito.TokenValidationParameters.NameClaimType"],
RoleClaimType = ConfigurationManager.AppSettings["Cognito.TokenValidationParameters.RoleClaimType"],
ValidateIssuer = validateIssuer
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = async (context) =>
{
var removePortOnRedirectIdentifierValue = ConfigurationManager.AppSettings["Cognito.RemovePortOnRedirectToIdentityProvider"];
var convertResult = bool.TryParse(removePortOnRedirectIdentifierValue, out var removePortOnRedirectIdentifier);
if (removePortOnRedirectIdentifier && convertResult)
{
var builder = new UriBuilder(context.ProtocolMessage.RedirectUri)
{
Scheme = "https",
Port = -1
};
context.ProtocolMessage.RedirectUri = builder.ToString();
}
}
},
Scope = ConfigurationManager.AppSettings["Cognito.Scope"],
SignInAsAuthenticationType = signInAsType
}
);
}
My .NET Framework MVC App:
public void ConfigureAuth(IAppBuilder app)
{
var loginPath = AuthorizationSettings.Instance.LoginPath;
if (string.IsNullOrEmpty(loginPath))
throw new ArgumentNullException("No value specified for EiHubSettings LoginPath");
// Configure the db context, user manager and signin manager to use a single instance per request
app.CreatePerOwinContext(SecurityDbContext.Create);
app.CreatePerOwinContext<SecurityUserManager>(SecurityUserManager.Create);
app.CreatePerOwinContext<SecurityRoleManager>(SecurityRoleManager.Create);
app.CreatePerOwinContext<SecuritySignInManager>(SecuritySignInManager.Create);
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
// Configure the sign in cookie
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString(loginPath),
CookieManager = new SystemWebCookieManager(),
ExpireTimeSpan = TimeSpan.FromSeconds(
Convert.ToInt32(ConfigurationManager.AppSettings["Cognito.CookieLifetimeInSeconds"])),
Provider = new CookieAuthenticationProvider
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<SecurityUserManager, SecurityUser, int>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentityCallback: (manager, user) => user.CreateIdentityAsync(manager),
getUserIdCallback: (user) => user.GetUserId<int>())
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process.
app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
// Enables the application to remember the second login verification factor such as phone or email.
// Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from.
// This is similar to the RememberMe option when you log in.
app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
if(AppSettingsConfigSingleton.UsesCognito)
{
ConfigureIdentityProviders(app, DefaultAuthenticationTypes.ApplicationCookie);
}
}
private void ConfigureIdentityProviders(IAppBuilder app, string signInAsType)
{
var saveTokens = true;
var validateIssuer = true;
var saveTokensValue = ConfigurationManager.AppSettings["Cognito.SaveTokens"];
if (!string.IsNullOrEmpty(saveTokensValue))
{
saveTokens = bool.TryParse(saveTokensValue, out var outResult) && outResult;
}
var validateIssuerValue = ConfigurationManager.AppSettings["Cognito.TokenValidationParameters.ValidateIssuer"];
if (!string.IsNullOrEmpty(validateIssuerValue))
{
validateIssuer = bool.TryParse(validateIssuerValue, out var outResult) && outResult;
}
app.UseCustomOidcAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = ConfigurationManager.AppSettings["Cognito.ClientId"],
ResponseType = ConfigurationManager.AppSettings["Cognito.ResponseType"],
Authority = ConfigurationManager.AppSettings["Cognito.Authority"],
MetadataAddress = ConfigurationManager.AppSettings["Cognito.MetadataAddress"],
ClientSecret = ConfigurationManager.AppSettings["Cognito.ClientSecret"],
RedirectUri = ConfigurationManager.AppSettings["Cognito.RedirectUri"],
SaveTokens = saveTokens,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = context =>
{
var redirectUri = "/account/openidlogincallback";
if (!string.IsNullOrEmpty(context.AuthenticationTicket.Properties.RedirectUri))
{
redirectUri += $"?returnUrl={context.AuthenticationTicket.Properties.RedirectUri}";
}
context.AuthenticationTicket.Properties.RedirectUri = redirectUri;
return Task.FromResult(0);
},
RedirectToIdentityProvider = context =>
{
var removePortOnRedirectIdentifierValue = ConfigurationManager.AppSettings["Cognito.RemovePortOnRedirectToIdentityProvider"];
var convertResult = bool.TryParse(removePortOnRedirectIdentifierValue, out var removePortOnRedirectIdentifier);
if (removePortOnRedirectIdentifier && convertResult)
{
var builder = new UriBuilder(context.ProtocolMessage.RedirectUri)
{
Scheme = "https", Port = -1
};
context.ProtocolMessage.RedirectUri = builder.ToString();
}
return Task.FromResult(0);
}
},
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = ConfigurationManager.AppSettings["Cognito.TokenValidationParameters.NameClaimType"],
RoleClaimType = ConfigurationManager.AppSettings["Cognito.TokenValidationParameters.RoleClaimType"],
ValidateIssuer = validateIssuer
},
Scope = ConfigurationManager.AppSettings["Cognito.Scope"],
SignInAsAuthenticationType = signInAsType
}
);
}
The .NET Core 3.1 app (yes, I know we need to upgrade to .NET 6), has a "built-in" mechanism for adding PKCE (which befuddled me for the longest time until I saw this -- this option is not explicitly set in my code, but as you can see, it's by default turned on):
So, I'm hoping against hope, that there is something similar in the .NET Framework

In the StartUp class:
// Referred to in CustomOidcHandler
public const string CODE_VERIFIER = "code_verifier";
private const string _CODE_CHALLENGE = "code_challenge";
private const string _CODE_CHALLENGE_METHOD = "code_challenge_method";
private const string _CODE_CHALLENGE_METHOD_S256 = "S256";
In the ConfigureIdentityProviders in Startup
RedirectToIdentityProvider = (context) =>
{
var removePortOnRedirectIdentifierValue = ConfigurationManager.AppSettings["Cognito.RemovePortOnRedirectToIdentityProvider"];
var convertResult = bool.TryParse(removePortOnRedirectIdentifierValue, out var removePortOnRedirectIdentifier);
if (removePortOnRedirectIdentifier && convertResult)
{
var builder = new UriBuilder(context.ProtocolMessage.RedirectUri)
{
Scheme = "https",
Port = -1
};
context.ProtocolMessage.RedirectUri = builder.ToString();
}
//
// We need the code_verifier value passed to the CustomOidcHandler
// so that it can be added to the POST to the /token endpoint
//
string codeVerifier = GenerateCodeVerifier();
HttpCookie cookie = new HttpCookie(CODE_VERIFIER);
cookie.Value = codeVerifier;
HttpContext.Current.Response.Cookies.Add(cookie);
//
// Add the code_challenge to the GET request to Cognito
//
// Methodology found here: https://github.com/IdentityServer/IdentityServer4/issues/4874
//
string codeChallenge = GenerateCodeChallenge(codeVerifier);
context.ProtocolMessage.SetParameter(_CODE_CHALLENGE, codeChallenge);
context.ProtocolMessage.SetParameter(_CODE_CHALLENGE_METHOD, _CODE_CHALLENGE_METHOD_S256);
return Task.CompletedTask;
}
In my CustomOidcHandler file, the GetTokens method:
string codeVerifier = Request != null && Request.Cookies != null && Request.Cookies[Startup.CODE_VERIFIER] != null ? Request.Cookies[Startup.CODE_VERIFIER] : string.Empty;
Dictionary<string, string> post = new Dictionary<string, string>
{
{"client_id", options.ClientId},
{"client_secret", options.ClientSecret},
{"grant_type", "authorization_code"},
{"code", authorizationCode},
{"redirect_uri", options.RedirectUri},
{Startup.CODE_VERIFIER, codeVerifier}
};
Hence, the only way is to "manually" add it.

Related

How to use keycloak in .net core 5.0 with AspNet Core Identity?

I am using aspnet core 5.0 webapi with CQRS in my project and already have jwt implementation. Not using role management from aspnet core but manually added for aspnet users table role field and it is using everywhere. In internet I can't find any article to implement keycloak for existing authentication and authorization. My point is for now users login with their email+password, idea is not for all but for some users which they already stored in keycloak, or for some users we will store there, give option login to our app using keycloak as well.
Scenario 1:
I have admin#gmail.com in both in my db and in keycloak and both are they in admin role, I need give access for both to login my app, first scenario already working needs implement 2nd scenarion beside first.
Found only this article which implements securing app (as we have already and not trying to replace but extend)
Medium keycloak
My jwt configuration looks like:
public static IServiceCollection AddCustomAuthentication(this IServiceCollection services,
IConfiguration configuration)
{
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(configuration.GetSection("AppSettings:Token").Value));
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opt =>
{
opt.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = key,
ValidateAudience = false,
ValidateIssuer = false,
ClockSkew = TimeSpan.Zero
};
opt.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
}
return Task.CompletedTask;
}
};
});
return services;
}
My jwt service looks like:
public JwtGenerator(IConfiguration config)
{
_key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.GetSection("AppSettings:Token").Value));
}
public string CreateToken(User user)
{
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, user.Id),
new(ClaimTypes.Email, user.Email),
new(ClaimTypes.Name, user.UserName),
new(ClaimTypes.Role, user.Role.ToString("G").ToLower())
};
var creds = new SigningCredentials(_key, SecurityAlgorithms.HmacSha512);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddMinutes(15),
SigningCredentials = creds
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
My login method looks like:
public async Task<GetToken> Handle(LoginCommand request, CancellationToken cancellationToken)
{
var user = await _userManager.FindByEmailAsync(request.Email);
if (user == null)
throw new BadRequestException("User not found");
UserManagement.ForbiddenForLoginUser(user);
var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, false);
if (result.Succeeded)
{
user.IsRoleChanged = false;
RefreshToken refreshToken = new RefreshToken
{
Name = _jwtGenerator.GenerateRefreshToken(),
DeviceName = $"{user.UserName}---{_jwtGenerator.GenerateRefreshToken()}",
User = user,
Expiration = DateTime.UtcNow.AddHours(4)
};
await _context.RefreshTokens.AddAsync(refreshToken, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
return new GetToken(_jwtGenerator.CreateToken(user),refreshToken.Name);
}
throw new BadRequestException("Bad credentials");
}
My authorization handler:
public static IServiceCollection AddCustomMvc(this IServiceCollection services)
{
services.AddMvc(opt =>
{
var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
opt.Filters.Add(new AuthorizeFilter(policy));
// Build the intermediate service provider
opt.Filters.Add<CustomAuthorizationAttribute>();
}).AddFluentValidation(cfg => cfg.RegisterValidatorsFromAssemblyContaining<CreateProjectCommand>());
return services;
}
What is best practise to implement keycloak authentiaction+authorization beside my current approach and give users to login with two scenarios, normal and keycloak login.
P.S. Ui is different and we are using angular this one just webapi for backend.
Since your login method returns a jwt, you could configure multiple bearer tokens by chaining .AddJwtBearer(), one for your normal login and one for keycloak.
Here is a link to a question that might solve your problem: Use multiple jwt bearer authentication.
Keycloak configuration:
Go to Roles -> Realm Roles and create a corresponding role.
Go to Clients -> Your client -> Mappers.
Create a new role mapper and select "User Realm Role" for Mapper Type, "roles" for Token Claim Name and "String" for Claim JSON Type. Without the mapping the role configured before would be nested somewhere else in the jwt.
You can use the debugger at jwt.io to check if your token is correct. The result should look like this:
{
"exp": 1627565901,
"iat": 1627564101,
"jti": "a99ccef1-afa9-4a62-965b-15e8d33de7de",
// [...]
// roles nested in realm_access :(
"realm_access": {
"roles": [
"offline_access",
"uma_authorization",
"Admin"
]
},
// [...]
// your mapped roles in your custom claim
"roles": [
"offline_access",
"uma_authorization",
"Admin"
]
// [...]
}

Need help adjusting ASP.NET that uses Azure AD to also allow SSO from third party

We have a ASP.NET application that uses Microsoft's common login to log in the users, then it redirects back to our web application (in Azure). The authentication is connected to Azure Active Directories. It is a multi-tenant application with multiple Azure ADs. When Microsoft redirects back to our site, we use the information to create a cookie that is used for the web calls. In addition, the call back returns a user code that we use to get a token. This is used as authentication against our API controllers.
This has been working for a long time. Now, however, we need to integrate with another 3rd party portal that will launch our product. They will be using SAML for SSO. They are not integrated with Azure AD. So, the idea is that we validate the users via the SAML assertions. That will contain the username that we then want to "log in" with.
I can create the cookie off of this information and that works fine with our web controller calls. However, since I'm not getting a callback from Azure AD, I don't have the token for the API calls. I have tried to call Azure AD to authenticate the applications, but that does seem to satisfy the API Controller's authorization. Specifically, RequestContext.Principal.Identity doesn't seem to be set with this pattern.
I have set the cookie authentication with this code:
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
var cookieOptions = new CookieAuthenticationOptions
{
ExpireTimeSpan = TimeSpan.FromDays(14),
SlidingExpiration = true,
LoginPath = new PathString("/home/login"),
AuthenticationType = "ApplicationCookie",
AuthenticationMode = AuthenticationMode.Active,
CookieHttpOnly = true,
CookieSecure = CookieSecureOption.Always,
CookieSameSite = SameSiteMode.Lax,
};
// Forms/Cookie Authentication
app.UseCookieAuthentication(cookieOptions);
And I left the bearer token authentication code like this:
// Bearer Token Authentication used for API access
BearerAuthenticationOptions = new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Tenant = <application tenant id>,
AuthenticationType = OAuthDefaults.AuthenticationType,
// Disable Issuer validation. We'll validate the Isuuer in the ClaimsAuthorizationFilter.
TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = <resource id>,
ValidateIssuer = false
},
};
app.UseWindowsAzureActiveDirectoryBearerAuthentication(BearerAuthenticationOptions);
The code that handles the Azure AD auth (that the SAML login should replace) is:
var openIdConnectOptions = new OpenIdConnectAuthenticationOptions { ClientId = <ClientId>, Authority = "https://login.windows.net/common/", // setting this to false uses the cookie expiration instead UseTokenLifetime = false, TokenValidationParameters = new TokenValidationParameters { // we'll validate Issuer on the SecurityTokenValidated notification below ValidateIssuer = false },
Notifications = new OpenIdConnectAuthenticationNotifications { // An AAD auth token is validated and we have a Claims Identity SecurityTokenValidated = context => { ... additional validation is performed here...
return Task.FromResult(0); },
//the user has just signed in to the external auth provider (AAD) and then were redirected here // with an access code that we can use to turn around and acquire an auth token AuthorizationCodeReceived = context => { var code = context.Code; var identity = context.AuthenticationTicket.Identity;
var appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase; // causes the retreived API token to be cached for later use TokenService.GetUserLevelTokenFromAccessCode(new HttpUserSessionWithClaimsId(identity), code, <ApiResourceId>, new Uri(appBaseUrl));
return Task.FromResult(0); }, // We are about to redirect to the identity provider (AAD) RedirectToIdentityProvider = context => { // This ensures that the address used for sign in and sign out is picked up dynamically from the request // Remember that the base URL of the address used here must be provisioned in Azure AD beforehand. var appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase; context.ProtocolMessage.RedirectUri = appBaseUrl + "/"; context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl;
context.HandleResponse();
return Task.FromResult(0); }, // Something went wrong during this auth process AuthenticationFailed = context => { if (context.Exception is Microsoft.IdentityModel.Protocols.OpenIdConnect
.OpenIdConnectProtocolInvalidNonceException) {
//This is a message we can't do anything about, so we want to ignore it.
Log.Info("AuthenticationFailed in OpenIdConnectAuthentication middleware", context.Exception); } else {
Log.Error("AuthenticationFailed in OpenIdConnectAuthentication middleware",
context.Exception); }
// IDX10205 == Tenant validation failed var message = (context.Exception.Message.StartsWith("IDX10205"))
? InvalidTenantMessage
: GenericErrorMessage;
context.OwinContext.Response.Redirect("/?Error=" + Uri.EscapeDataString(message)); context.HandleResponse(); // Suppress the exception return Task.FromResult(0); }, MessageReceived = context => { if (!string.IsNullOrWhiteSpace(context.ProtocolMessage.Error)) {
// AADSTS65004 == user did not grant access in OAuth flow
Log.Error("MessageReceived containing error in OpenIdConnectAuthentication middleware. \nError: {0}\nDescription: {1}"
.FormatWith(context.ProtocolMessage.Error, context.ProtocolMessage.ErrorDescription));
//context.OwinContext.Response.Redirect("/");
//context.HandleResponse(); // Suppress the exception } return Task.FromResult(0); } } };
app.UseOpenIdConnectAuthentication(openIdConnectOptions);
Any help would be greatly appreciated.
Turns out the value I put for AuthenticationType in the cookie was causing the disconnect. Once I fixed that, the data came through. So closing his question.
Thanks.

ASP core login through external (custom) service

I am looking for proper way how to implement authentification and authorization.
If I understand it well - this should be realized through "Identity" - it's offering both of these things I need.
My problem is that i can't use a database. I have to use a service (WCF service where our internal DDLs are connected to our system) which is able only Login (I give it user name and password) and after login i can get list of permissons.
I already saw articles how to have custom UserStore, RoleStore, UserManager and SignInManager.. but I am still confused and I don't know how to do it.
Is this even posible through Identity model? If not how I should do it please?
Thank you for every advice.
There are some articles which I already checked:
Microsoft - custom storage providers
Core indentity without entity framework
Sikorsky blog - custom user manager
In fact the WCF service is your authentication service. No need to implement this twice. A call to the service verifies the credentials and returns everything that should be in the access token.
Basically all you have to do is generate an access token using the information from the WCF service. And configure your app to use the created access token.
The flow can be like this: first make a call to the WCF service in order to verify the login and retrieve the information.
public async Task<IActionResult> LoginAsync([FromBody]UserLogin login)
{
var loginInfo = _wcf.LoginUser(login);
if (loginInfo == null)
return Unauthorized();
return Ok(CreateAccessToken(loginInfo));
}
To create an access token:
public class TokenHelper
{
public const string Issuer = "http://www.mywebsite.com/myapp";
public const string Audience = "http://www.mywebsite.com/myapp";
// This should not be hardcoded!
public const string Secret = "My_super_secret";
public AccessToken CreateAccessToken(LoginInfo loginInfo)
{
// Set expiration time of 5 minutes.
DateTime expires = DateTime.UtcNow.AddMinutes(5);
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, loginInfo.UserId),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
// Add custom claims, rolepermissions
if (loginInfo.Permissions != null && loginInfo.Permissions.Any())
loginInfo.Permissions.foreach(p => claims.Add(new Claim("Permission", p)));
if (loginInfo.IsUser)
claims.Add(new Claim(ClaimTypes.Role, "User"));
if (loginInfo.IsAdmin)
claims.Add(new Claim(ClaimTypes.Role, "Admin"));
var token = new JwtSecurityToken(
issuer: Issuer,
audience: Audience,
claims: claims,
expires: expires,
signingCredentials: new SigningCredentials(
new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Secret)),
SecurityAlgorithms.HmacSha256
)
);
return new AccessToken
{
ServerTime = DateTime.UtcNow.ToString("yyyyMMddTHH:mm:ssZ"),
Expires = expires.ToString("yyyyMMddTHH:mm:ssZ"),
Bearer = new JwtSecurityTokenHandler().WriteToken(token)
};
}
}
Where AccessToken is:
public class AccessToken
{
public string ServerTime { get; set; }
public string Expires { get; set; }
public string Bearer { get; set; }
}
And add authentication in your startup:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
RequireExpirationTime = true,
RequireSignedTokens = true,
ValidIssuer = TokenHelper.Issuer,
ValidAudience = TokenHelper.Audience,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.ASCII.GetBytes(
TokenHelper.Secret))
};
}
);
-- update --
_issuer, _audience and _secret are from some 'external source'. Meaning that all three are fixed string values, but the source (where the value is set) is variable.
For _issuer you usually use the url of the server that issues the token. Like http://www.mywebsite.com/myapp
The _audience is the application that is meant to accept the token. In this case _issuer and _audience are the same, so you can use the same value.
_secret is, well secret and can be any string, like 'my_super_secret'. This is something you want to stay secret. So you don't hardcode it, but get it from a safe location instead.
I've updated above code in a way so you can test it. Please note that secret should not be hardcoded.

ASP.NET CORE Sign In Cookie

I am having a lot of trouble understanding some things in Asp.NET Core. I already have a Asp.NET 4.5 application that has login authentication using FormAuthenticationTicket but my goal is to set up a Core Web Api that authenticates a user and creates a cookie for my 4.5 Application to read, and on redirect to already be signed in via cookie.
I have given both applications the same <machinekey> in the web.config and added UseCookieAuthentication with CookieAuthenticationOptions to Startup.cs but I am at a loss from here on how to replicate the FormsAuthenticationTicket inside my ApplicationController.cs in my Core application. I find that the documentation for Core is not overly consistant yet but I have been trying out a lot of suggestions to no avail.
I think the main confusion for me is that I can create a cookie in Core I am clearly not creating it correctly or most likely not authenticating correctly either.
Startup.cs in Configure function
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = "ApiAuth",
CookieName = ".ASPXAUTH",
CookieHttpOnly = false,
ExpireTimeSpan = TimeSpan.FromDays(30),
SlidingExpiration = true,
AutomaticAuthenticate = true,
LoginPath = new PathString("/Application/Authorize"),
});
ApplicationController.cs
[HttpGet("Authorize/{appGuid}/{userGuid}", Name = "SignIn")]
public async Task<IActionResult> SignIn(Guid appGuid, Guid userGuid)
{
var application = Application.Find(appGuid);
var user = User.Find(userGuid);
if (application != null && user != null)
{
await HttpContext.Authentication.SignOutAsync("ApiAuth");
/****************Confusion start****************/
Claim cookiePath = new Claim(ClaimTypes.CookiePath, ".ASPXAUTH");
Claim expiration = new Claim(ClaimTypes.Expiration, DateTime.UtcNow.AddDays(30).ToString());
Claim expiryDate = new Claim(ClaimTypes.Expired, "false");
Claim persistant = new Claim(ClaimTypes.IsPersistent, "true");
Claim issueDate = new Claim("IssueDate", DateTime.UtcNow.ToString());
Claim name = new Claim(ClaimTypes.Name, user.Username);
Claim userData = new Claim(ClaimTypes.UserData, "");
Claim version = new Claim(ClaimTypes.Version, "2");
ClaimsPrincipal principal = new ClaimsPrincipal(new ClaimsIdentity(new[] { cookiePath, expiration, expiryDate,
persistant, issueDate, name, userData, version }, "ApiAuth"));
await HttpContext.Authentication.SignInAsync("ApiAuth", principal);
/****************Confusion end****************/
return new RedirectResult("http://localhost/MyWebsite/Repository.aspx");
}
return Unauthorized();
}
The size of the cookie is much larger than the one on my 4.5 application and I am at a loss as to where to go from here. I believe I am also causing conflicting settings with UseCookieAuthentication and the ClaimsPrincipal.

IdentityServer3 + AzureAD and RedirectUri Confusion

I'm struggling to understand how IdentityServer3, AzureAD and a Private Database all work together. The biggest problem is how the Redirect URIs are being handled.
My scenario is I have a stand alone IdentityServer3. It's job is to authenticate users against either AzureAD or a private DB. Within the Startup.cs file on the ID3 server, I have the following OpenID Connect code:
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.Map("/identity", s3App =>
{
s3App.UseIdentityServer(new IdentityServerOptions
{
SiteName = "3S",
SigningCertificate = Certificate.Load(),
Factory = new IdentityServerServiceFactory()
.UseInMemoryUsers(InMemoryUsers.Get())
.UseInMemoryClients(InMemoryClients.Get())
.UseInMemoryScopes(InMemoryScopes.Get()),
AuthenticationOptions = new AuthenticationOptions
{
EnablePostSignOutAutoRedirect = true,
EnableSignOutPrompt = false,
IdentityProviders = ConfigureAdditionalIdentityProviders
}
});
});
}
public static void ConfigureAdditionalIdentityProviders(IAppBuilder app, string signInAsType)
{
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "AzureAd",
Caption = "Login",
ClientId = "4613ed32-xxxx-xxxx-xxxx-xxxxxxxxxxxx", // GUID of registered application on Azure
Authority = "https://login.microsoftonline.com/our-tenant-id/",
PostLogoutRedirectUri = "https://localhost:44348/identity",
RedirectUri = "https://localhost:44348/identity",
Scope = "openid email profile",
ResponseType = "id_token",
AuthenticationMode = AuthenticationMode.Passive,
SignInAsAuthenticationType = signInAsType,
TokenValidationParameters = new TokenValidationParameters
{
AuthenticationType = Constants.ExternalAuthenticationType,
ValidateIssuer = false
}
});
}
I don't understand why the ID3 Server would need to have either RedirectUri or PostLogoutRedirectUri...shouldn't that be "passed through" from the application requesting the authentication? After all, we want to get back to the application, not the ID3 Server. Granted, I don't think this is what's causing my problem, but it would be nice to understand why these are here.
I will say, I've gotten "close" to this working.
When my application requiring authentication requests authentication against AzureAD, I'm redirected to the Microsoft Account login screen to enter my username/password for my work account. I submit my credentials and then get redirected back to either the ID3 server or my application, depending on which RedirectUri has been used in the above code.
For the sake of argument, let's say I use my application for the RedirectUri. I will be sent back to the application, but not to the page that initially prompted the authentication challenge, and if I click on a page that requires authentication, I'm sent back to the AzureAD server to log in again, only this time AzureAD recognizes me as already logged in.
Unfortunately, it doesn't appear that the SecurityTokenValidated notification is being acknowledged/set after the redirect from AzureAD.
Here's the code found in the application Startup.cs:
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44348/identity",
ClientId = "3af8e3ba-5a04-4acc-8c51-1d30f8587ced", // Local ClientID registered as part of the IdentityServer3 InMemoryClients
Scope = "openid profile roles",
RedirectUri = "http://localhost:52702/",
PostLogoutRedirectUri = "http://localhost:52702/",
ResponseType = "id_token",
SignInAsAuthenticationType = "Cookies",
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = n =>
{
var id = n.AuthenticationTicket.Identity;
var givenName = id.FindFirst(Constants.ClaimTypes.GivenName);
var familyName = id.FindFirst(Constants.ClaimTypes.FamilyName);
var sub = id.FindFirst(Constants.ClaimTypes.Subject);
var roles = id.FindAll(Constants.ClaimTypes.Role);
var nid = new ClaimsIdentity(
id.AuthenticationType,
Constants.ClaimTypes.GivenName,
Constants.ClaimTypes.Role
);
nid.AddClaim(givenName);
nid.AddClaim(familyName);
nid.AddClaim(sub);
nid.AddClaims(roles);
nid.AddClaim(new Claim("application_specific", "Some data goes here. Not sure what, though."));
nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
n.AuthenticationTicket = new AuthenticationTicket(nid, n.AuthenticationTicket.Properties);
return Task.FromResult(0);
},
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType != OpenIdConnectRequestType.LogoutRequest)
return Task.FromResult(0);
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
}
return Task.FromResult(0);
},
AuthenticationFailed = (context) =>
{
context.HandleResponse();
context.Response.Redirect("/Error/message=" + context.Exception.Message);
//Debug.WriteLine("*** AuthenticationFailed");
return Task.FromResult(0);
},
}
});
AntiForgeryConfig.UniqueClaimTypeIdentifier = Constants.ClaimTypes.Subject;
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
}
}
You'll notice that the OpenIdConnectAuthenticationOptions also contain a RedirectUri and a PostLogoutRedirectUri which point to the application, but those don't seem to matter.
Of course, everything works perfectly when I'm logging in using the "cookies" side of things - I see all of my claims for the user. And, spending some time on the phone with Microsoft, they proposed a solution outside of ID3 which worked, but is not the way we need to go. We will have multiple applications authenticating against our ID3 so we need to contain and control the flow internally.
I really need some help trying to figure out this last mile issue. I know I'm close, I've just been staring at this so long that I'm probably staring right at my error and not seeing it.
10/22/2016 Edit
Further testing and enabling Serilog revealed an issue with the RedirectUri and PostLogoutRedirectUri resulted in my adding the /identity to the end of the URIs which corresponds to the value set in app.Map. This resolved the issue of my being returned to the "blank" page of IdentityServer3, I'm now returned to the IdentityServer3 login screen. Azure AD still thinks I'm logged in, I'm just not getting the tokens set properly in my application.
Since the authenticate flow is a little complex, I am trying illustrate it using a figure below:
First the redirect URL need to register to the identity provider, so the server will match the RedirectURL in request to ensure that the response is redirected as expected instead of redirect to like Phishing site( security consideration).
And as the figure demonstrate, to use the Azure AD as the external identity provider for IdentityServer3, we need to register the apps on Azure AD. However since the app is used to communicate with Identity Server, the redirect URL register on the Azure Portal should redirect to the IdentityServer3 instead of URL of app.
For example, the URL of my identity server 3 is https://localhost:44333 then I use code below to add the additional identity providers. And this URL is the redirect URL on the Azure portal:
public static void ConfigureAdditionalIdentityProviders(IAppBuilder app, string signInAsType)
{
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "aad",
Caption = "Azure AD",
SignInAsAuthenticationType = signInAsType,
Authority = "https://login.microsoftonline.com/04e14a2c-0e9b-42f8-8b22-3c4a2f1d8800",
ClientId = "eca61fd9-f491-4f03-a622-90837bbc1711",
RedirectUri = "https://localhost:44333/core/aadcb",
});
}
And the URL of my app is http://localhost:1409/ which is register on the IdentyServer3 and below code is the web app use OWIN OpenId Connect to add the IdentyServer3 as the identity data provider:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "oidc",
SignInAsAuthenticationType = "cookies",
Authority = "https://localhost:44333",
ClientId = "mvc",
RedirectUri = "http://localhost:1409/",
ResponseType = "id_token",
Scope = "openid profile email"
});

Categories