IdentityServer4 Session Cookie Management (how to do it properly?) - c#

We are working on an idetityserver4 (A SPA application in angular) that will run on a standalone server and will comunicate with an API(asp.net API) that is on another server, the patern we are trying to implement is BFF (backend for front end) and if we didn't misunderstand the concept badly, our ID4 will act as the gateway to the API, firstly we log to the ID4 with the ClientID and secret so that we get a token and generate the session cookies, afterward we forward everything to the API to complete the local login.
Everything works fine till here, we get the response we want, token gets set to acces the API and cookies generated for that client are automatically returned in Header (we use POSTMAN), but when i try to make a request to the API (request that firstly goes to ID4 to get verified if the client cookie is not expired) apparently ID4 does not verify if the client cookie is or not expired, i can make as many requests as i like....
We really don't understand how to do it properly and we didn't find something that can help us...
Here are the snippets
Startup.cs
services.AddAuthentication(options =>
{
options.DefaultScheme = "cookies";
options.DefaultChallengeScheme = "oidc";
options.RequireAuthenticatedSignIn = true;
})
.AddCookie("cookies", options =>
{
options.Cookie.Name = "cookie-bff";
options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(1);
options.Cookie.SecurePolicy = 0;
options.SlidingExpiration = true;
options.Events.OnSigningOut = async e =>
{
// revoke refresh token on sign-out
await e.HttpContext.RevokeUserRefreshTokenAsync();
};
})
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://localhost:4001/";
options.ClientId = "angular-client";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
/*
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("api");
options.Scope.Add("offline_access");
*/
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
services.AddAccessTokenManagement(options =>
{
// client config is inferred from OpenID Connect settings
// if you want to specify scopes explicitly, do it here, otherwise the scope parameter will not be sent
options.Client.Scope = "write";
})
.ConfigureBackchannelHttpClient();
services.AddUserAccessTokenClient("user_client", client =>
{
client.BaseAddress = new Uri("https://localhost:5001/api/");
});
services.AddClientAccessTokenClient("client", configureClient: client =>
{
client.BaseAddress = new Uri("https://localhost:5001/api/");
});
Where we configure the Context for operation and configuration
services.AddIdentityServer(options => {
options.Authentication.CheckSessionCookieName = "cookie-bff";
options.Authentication.CookieLifetime = TimeSpan.FromMinutes(1);
options.Authentication.CookieSameSiteMode = SameSiteMode.Strict;
options.Authentication.CookieSlidingExpiration = true;
options.LowerCaseIssuerUri = false;
options.EmitScopesAsSpaceDelimitedStringInJwt = false;
})
.AddDeveloperSigningCredential()
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(configuration.GetConnectionString("LocalDevelopment"),
sql => sql.MigrationsAssembly(migrationAssembly));
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(configuration.GetConnectionString("LocalDevelopment"),
sql => sql.MigrationsAssembly(migrationAssembly));
});
This is our LoginController(its work in progres, we are beginers)
[HttpPost]
[Route("/identityserver/login")]
public async Task<IActionResult> Post([Required, FromBody] LoginPostDTO Json)
{
var client = new HttpClient();
var response = await client.RequestTokenAsync(new TokenRequest
{
Address = "https://localhost:4001/connect/token",
GrantType = "client_credentials",
ClientId = "angular-client",
ClientSecret = "secret",
Parameters =
{
{ "scope", "read"},
{"openid","profile"}
}
});
var claims = new List<Claim>
{
new Claim(JwtClaimTypes.Name,"Company"),
new Claim(JwtClaimTypes.Role, "Administrator"),
new Claim(JwtClaimTypes.Subject, "Company")
};
var claimsIdentity = new ClaimsIdentity(claims, "cookies");
var authProperties = new AuthenticationProperties
{
AllowRefresh = true,
// Refreshing the authentication session should be allowed.
ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(1),
// The time at which the authentication ticket expires. A
// value set here overrides the ExpireTimeSpan option of
// CookieAuthenticationOptions set with AddCookie.
IsPersistent = false,
// Whether the authentication session is persisted across
// multiple requests. When used with cookies, controls
// whether the cookie's lifetime is absolute (matching the
// lifetime of the authentication ticket) or session-based.
IssuedUtc = DateTime.Now,
// The time at which the authentication ticket was issued.
RedirectUri = "https://localhost:4001/signin-oidc"
// The full path or absolute URI to be used as an http
// redirect response value.
};
await HttpContext.SignInAsync("cookies", new ClaimsPrincipal(claimsIdentity), authProperties);
var stringContent = new StringContent(JsonConvert.SerializeObject(Json), Encoding.UTF8, "application/json");
var api = new HttpClient();
api.SetBearerToken(response.AccessToken);
var apiResponse = await api.PostAsync("https://localhost:5001/api/login", stringContent);
return Ok(apiResponse.Content.ReadAsAsync(typeof(JObject)).Result);
}
}
And here is the Controller where we comunicate with the API after we login
public class Controller : ControllerBase
{
[HttpPost]
[Route("/identityserver/request")]
[Produces("application/json")]
[Consumes("application/json")]
public async Task<IActionResult> Post([Required, FromBody]DTO Json)
{
var stringContent = new StringContent(JsonConvert.SerializeObject(Json), Encoding.UTF8, "application/json");
var api = new HttpClient();
api.SetBearerToken(await HttpContext.GetClientAccessTokenAsync());
var apiResponse = await api.PostAsync("https://localhost:5001/api/request/medical-request", stringContent);
return Ok(apiResponse.Content.ReadAsAsync(typeof(JObject)).Result);
}
}
Thank you in advance!
P.S This is my first post on stack :)

Related

ASP .NET Core custom authentication scheme with external login

I have a website where users log in on an external website and are redirected back with a token in the querystring. I then validate the token and create the authentication ticket. How would I check on subsequent requests that the user is now logged in? And how would I log a user out?
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var token = Request.Query["token"].FirstOrDefault();
if (token != null)
{
var validator = new JwtSecurityTokenHandler();
var region = "us-xxx-x";
var userPoolId = "us-xxx-xxxxxxx";
var appClientId = "xxxxxxxxxxxxxxxxxxxx";
var cognitoIssuer = $"https://cognito-idp.{region}.amazonaws.com/{userPoolId}";
var jwtKeySetUrl = $"{cognitoIssuer}/.well-known/jwks.json";
var validationParameters = new TokenValidationParameters
{
IssuerSigningKeyResolver = (s, securityToken, identifier, parameters) =>
{
var json = new WebClient().DownloadString(jwtKeySetUrl);
var keys = JsonConvert.DeserializeObject<JsonWebKeySet>(json).Keys;
return (IEnumerable<SecurityKey>)keys;
},
ValidIssuer = cognitoIssuer,
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateLifetime = true,
ValidAudience = appClientId
};
// validate the token
var principal = validator.ValidateToken(token, validationParameters, out var validatedToken);
if (principal.HasClaim(c => c.Type == ClaimTypes.NameIdentifier))
{
var claims = new[] { new Claim("token", token) };
var identity = new ClaimsIdentity(claims, nameof(TokenAuthenticationHandler));
var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), this.Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
else
return Task.FromResult(AuthenticateResult.Fail("Token validation failed"));
}
else
return Task.FromResult(AuthenticateResult.Fail("No token"));
}
I tried with the code:
In controller:
[Authorize(AuthenticationSchemes = "Cookies,Bearer")]
public IActionResult Index()
{
return View();
}
public IActionResult CreateToken()
{
var claims = new Claim[]
{
new Claim(ClaimTypes.Name, "Jeffcky"),
};
var token = GenerateToken(claims);
Response.Cookies.Append("x-access-token", token);
return RedirectToAction("Login");
}
private string GenerateToken(Claim[] claims)
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("1234567890123456"));
var token = new JwtSecurityToken(
issuer: "http://localhost:5000",
audience: "http://localhost:5001",
claims: claims,
notBefore: DateTime.Now,
expires: DateTime.Now.AddMinutes(5),
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
[HttpPost]
public async Task<IActionResult> Login(User user)
{
var token = Request.Cookies["x-access-token"];
var jsonToken = new JwtSecurityTokenHandler().ReadToken(token) as JwtSecurityToken;
var username = jsonToken.Claims.FirstOrDefault(m => m.Type == ClaimTypes.Name).Value;
if (username!=user.Name)
{
var claims = new Claim[]
{
new Claim(ClaimTypes.Name, "Jeffcky"),
};
var claimsIdentity = new ClaimsIdentity(claims, "Login");
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity));
}
return RedirectToAction("Index");
}
In startup:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie( m =>
{
m.LoginPath = new Microsoft.AspNetCore.Http.PathString("/Home/Login");
m.AccessDeniedPath = new Microsoft.AspNetCore.Http.PathString("....");
m.SlidingExpiration = true;
m.ExpireTimeSpan = TimeSpan.FromMinutes(120);
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("1234567890123456")),
ValidateIssuer = true,
ValidIssuer = "http://localhost:5000",
ValidateAudience = true,
ValidAudience = "http://localhost:5001",
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5)
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Cookies["x-access-token"];
if (!string.IsNullOrEmpty(accessToken))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
The Result:
you could modify the codes in login controller to pool data from database and varify the userinformation to meet your requirement.
And the Attribute[Authorize(AuthenticationSchemes = "Cookies,Bearer")] allows you Authentication with cookie or jwt,it may help in some situation
and if you want to logout,you could delete the cookie which strores the token or ticket
UpDate:
Accroding to your description,I think you could use jwt Authentication and set in your startup as follow:
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["tookn"];
if (!string.IsNullOrEmpty(accessToken))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
Is it the result you excepted?
Full disclosure, I'm not very knowledgeable on the library you are using or on what exactly you are trying to do here, but I can tell you how these things work, generally speaking. Conceptually logging in and out on a web service works as follows:
Client sends authentication information to the server (Username, password hash, 2FA info, etc.)
The server checks the authentication information against the stored information about the user (e.g. compare password hash to the stored password hash)
If authentication is sucessful, the server creates and stores a cookie (a long randomly generated string). The server stores the cookie along with information such as the user who the cookie belongs to, and when the cookie was created.
The server sends the cookie to the client. The client is now "logged in".
Now when the client wants to send some http request to the web service, they send the cookie along with the request. The server then looks up who the cookie belongs to and handles the http request appropriately.
When the user wants to log out they send an http request to the server asking to log out, and pass the cookie along with the message. The server then invalidates the cookie, and further http requests are handled as if the user is logged out.

AspNetCore API JWT Token works in PostMan but not with HttpClient

I am trying to call an API with HttpClient, and when I call the API without the [Authorize] I get all of the information correctly. when I add the attribute I do get a 401 unauthorized error. The funny thing is it works correctly when I call with the Bearer token from Postman.
I have seen a few posts where they say to add app.UseAuthentication(); before app.UseAuthorization(); which I have done.
Below is the code that I use to call the API, something I noticed is I am calling http://localhost:5000/api/accounts but I am getting a response from https://localhost:5001/api/accounts
protected HttpClient CreateClient()
{
var client = new HttpClient { BaseAddress = new Uri(ConfigurationManager.AppSettings["ServerAddress"]) };
if(_securityService.HasToken)
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _securityService.Token);
}
return client;
}
protected async Task<T> GetAsync<T>(string url, CancellationToken cancellationToken = default, params object[] parameters)
{
using var client = CreateClient();
if (parameters.Any())
url = string.Format(url, parameters);
var response = await client.GetAsync(url, cancellationToken);
if (response.IsSuccessStatusCode)
{
var stringContent = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<T>(stringContent);
}
else
{
//TODO: Add Logger and log Result
return default;
}
}
Client ApiAddress
Response Message
I have 2 questions here, why would this change a request uri, can someone please explain why this would work in postman, but not using HttpClient.
I thought about this and thought I should add the server code in here too.
services.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<SchedulerContext>()
.AddDefaultTokenProviders();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
var securityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Constants.ConstantsToAddToSecrets.Seceret));
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = securityKey,
ValidateIssuer = false,
ValidateAudience = false
};
});
services.AddAuthorization(options =>
{
options.DefaultPolicy =
new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build();
options.AddPolicy(Roles.Manager, policy => policy.RequireClaim(Roles.Manager));
options.AddPolicy(Roles.User, policy => policy.RequireClaim(Roles.User));
options.AddPolicy(Roles.Admin, policy => policy.RequireClaim(Roles.Admin));
options.InvokeHandlersAfterFailure = true;
});
services.Configure<IdentityOptions>(options =>
{
options.Password.RequireDigit = true;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireLowercase = false;
options.Password.RequireUppercase = false;
options.Password.RequiredLength = 6;
options.User.RequireUniqueEmail = false;
options.Tokens.AuthenticatorIssuer = ConstantsToAddToSecrets.Issuer;
});

Configure the return_type field in ASP.NET Core

I'm using the AuthenticationBuilder.AddOAuth(...) method to add OAuth functionality to my site. This is working perfectly for one service I am connecting to, but the same doesn't go for the second service.
Apparently, during the redirect to the authorization endpoint, the second service requires the URL parameter return_type to be set to token, my configuration sets it to code though. I haven't been able to find what part of the OAuthOptions determines this parameter so far.
I'll add the code I am using for convenience:
authBuilder.AddOAuth(Identifier, options =>
{
options.ClientId = ClientId;
options.ClientSecret = ClientSecret;
options.CallbackPath = new PathString($"/signin-{Identifier}");
options.AccessDeniedPath = new PathString("/");
options.AuthorizationEndpoint = Domain + AuthorizationEndpoint;
options.TokenEndpoint = Domain + TokenEndpoint;
options.UserInformationEndpoint = Domain + UserInfoEndpoint;
options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, NameIdentifierKey);
options.ClaimActions.MapJsonKey(ClaimTypes.Name, NameKey);
options.SaveTokens = true;
options.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode();
var user = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
context.RunClaimActions(user.RootElement);
}
};
options.Scope.Add(Scope);
});

How do I enforce username and password on token generation or is it bad practice

I have started a simple web api and added token generation to it using jwt. However I used in app accounts for the user store this is my function for setting up the token. Its showing up in swagger fine but what I dont get is how to I enforce the username and password to be entered when the request for token is made. Or is that bad practise.
This is my class when I generate the security token.
public JwtService(IConfiguration config)
{
var test = config;
_secret = config.GetSection("JwtToken").GetSection("SecretKey").Value;
_expDate = config.GetSection("JwtToken").GetSection("expirationInMinutes").Value;
}
public string GenerateSecurityToken(string email)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_secret);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Email, email)
})
,
Expires = DateTime.UtcNow.AddMinutes(double.Parse(_expDate)),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
}
public static IServiceCollection AddTokenAuthentication(this IServiceCollection services, IConfiguration config)
{
var secret = config.GetSection("JwtToken").GetSection("SecretKey").Value;
var keySecret = Base64UrlEncoder.DecodeBytes(secret);
var key = Encoding.ASCII.GetBytes(keySecret.ToString());
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
// ValidIssuer = "localhost",
//ValidAudience = "localhost"
};
});
return services;
}
Swagger Gen Code
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "App Manager - Running Buddies", Version = "v1" });
c.AddSecurityDefinition("bearer", new OpenApiSecurityScheme
{
Name = "Authorization",
Type = SecuritySchemeType.ApiKey,
Scheme = "bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description = "JWT Authorization header using the Bearer scheme.",
});
});
Swagger Ui Test
You might want to pass username/password in a model using HTTP POST.
Be sure to only issue a token when the login request is valid, i.e. after you have successfully authenticated the user.
See Securing ASP.NET Core 2.0 Applications with JWTs
for further details.
Edit: To perform a login using Identity you could use SignInManager.PasswordSignInAsync or to just check the credentials SignInManager.CheckPasswordSignInAsync. See the samples for an example:
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
// generate the token
}

.Net-Core Log out of Oauth

I've created an application which uses OAuth to login to Coinbase. My startup configuration looks like so:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = COINBASE_AUTH_ID;
})
.AddCookie()
.AddOAuth(COINBASE_AUTH_ID, options =>
{
options.ClientId = Configuration["Coinbase:ClientId"];
options.ClientSecret = Configuration["Coinbase:ClientSecret"];
options.CallbackPath = new PathString("/signin-coinbase");
options.AuthorizationEndpoint = "https://www.coinbase.com/oauth/authorize?meta[send_limit_amount]=1";
options.TokenEndpoint = "https://api.coinbase.com/oauth/token";
options.UserInformationEndpoint = "https://api.coinbase.com/v2/user";
COINBASE_SCOPES.ForEach(scope => options.Scope.Add(scope));
options.SaveTokens = true;
options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
options.ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
options.ClaimActions.MapJsonKey("urn:coinbase:avatar", "avatar_url");
options.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
request.Headers.Add("CB-VERSION", DateTime.Now.ToShortDateString());
var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode();
var user = JObject.Parse(await response.Content.ReadAsStringAsync());
context.RunClaimActions(user);
}
};
});
When a user logs in I return a challenge result and let the default authentication do it's work.
[HttpGet]
public IActionResult Login(string returnUrl = "/")
{
return Challenge(new AuthenticationProperties() { RedirectUri = returnUrl });
}
I'm trying to figure out how to logout but when I call Signout on the base controller nothing is happening.
[HttpGet]
public IActionResult Logout()
{
this.SignOut();
return Redirect(Url.Content("~/"));
}
How can I log out of oauth?
There's probably a more graceful way of doing this but for now. I found I can call the SignOut method on the HTTPContext
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync();
return Redirect(Url.Content("~/"));
}

Categories