Provider claims in IProfileService - c#

When I auth using oidc I get back a bunch of claims. If I do not add my custom IProfileService all of these claims are passed through in the id_token that identity server issues. If I provide my own ProfileService, the list of claims on the Subject is a subset of what comes back from the idp. Is there any way to get the full list in the profile service?
Here is the relevant info from Startup.cs:
var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
}).AddProfileService<ProfileService>();
services.AddAuthentication()
.AddOpenIdConnect("Name", "Name", o =>
{
o.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
o.SignOutScheme = IdentityServerConstants.SignoutScheme;
o.Authority = "https://sub.domain.com/adfs/";
o.ClientId = "00000000-0000-0000-0000-000000000000";
o.ClientSecret = "secret";
o.ResponseType = "id_token";
o.SaveTokens = true;
o.CallbackPath = "/signin-adfs";
o.SignedOutCallbackPath = "/signout-callback-adfs";
o.RemoteSignOutPath = "/signout-adfs";
o.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
and my ProfileService:
public class ProfileService : IProfileService
{
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var objectGuidClaim = context.Subject.Claims.FirstOrDefault(x => x.Type == "ObjectGUID");
if (objectGuidClaim != null)
{
var userId = new Guid(Convert.FromBase64String(objectGuidClaim.Value));
context.IssuedClaims.Add(new Claim("UserId", userId.ToString()));
}
return Task.CompletedTask;
}
public Task IsActiveAsync(IsActiveContext context)
{
context.IsActive = true;
return Task.CompletedTask;
}
}
So in my case, without the ProfileService then ObjectGUID is passed through, but using the ProfileService, it's not available in context.Subject.Claims list.
My goal is to take the "ObjectGUID" claim from the idp which is a base64 encoded guid and convert it to a hex string and pass that along as the "UserId" claim from identity server.
I'm not even sure this is the best way. I've also tried converting it through ClaimActions but my action never executes (I tested with a random guid to make sure it wasn't something with the conversion):
o.ClaimActions.MapCustomJson("UserId", obj => {
return Guid.NewGuid().ToString();
});
Is this a better way? Why is it not executing?

Try to:
ensure your Subject does not contain
http://schemas.company.com/identity/claims/objectguid instead of just ObjectGUID
extend your
OpenIdConnect configuration with: o.GetClaimsFromUserInfoEndpoint =
true; together with o.ClaimActions.MapUniqueJsonKey("ObjectGUID", "ObjectGUID"); or o.ClaimActions.MapUniqueJsonKey("http://schemas.company.com/identity/claims/objectguid", "ObjectGUID");
if nothing before helped, try:
o.Events = new OpenIdConnectEvents
{
OnTicketReceived = context =>
{
var identity = context.Principal.Identity as ClaimsIdentity;
StringBuilder builder = new StringBuilder();
var claims = identity?.Claims.Select(x => $"{x.Type}:{x.Value};");
if (claims != null)
builder.AppendJoin(", ", claims);
Logger.LogInformation($"Ticket received: [Claims:{builder}]");
identity?.AddClaim(new Claim("userId", Guid.NewGuid().ToString()));
//you can embed your transformer here if you like
return Task.CompletedTask;
}};
(you can examine the exact incoming ticket here and leave the logging anyway for future purposes)

Related

ASP.NET Core HttpxContext User is null but user is Authenticated

Basically, I can log in just fine in my Client and access all pages. If I log out, I can't access anything on my client, it always redirects to my login page. Directly accessing the api endpoint is another matter. Say I call mylocalapp.com/api/users, directly from the browser url bar, I can still get all data. So I added [Authorize]. Unexpectedly, I get a 401 if I do that, DESPITE being logged in. So I checked HttpContext.User and simply User.Identity and they're empty, despite successful authentication. ClaimsIdentity is also empty. Can you identify anything that I might have done wrong? According to what I found on Google, this IS how it should be done. Thanks for your help.
Login method:
Takes user data, checks if ok, creates a JWT token and gets browser cookies (if null - session expired) then appends the token to the browser. And then logs the user in via Sign In Manager. If ok, returns ok. Context var is just for debugging purposes.
[AllowAnonymous]
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
async public Task<IActionResult> LoginPost(AuthUser authUser)
{
var user = _context.Users.FirstOrDefault(user => user.Email == authUser.Email);
if (authUser is null) return BadRequest(AppResources.NullUser);
if (user is null) return BadRequest(AppResources.UserBadCredentials);
else
{
var isPassword = _userManager.CheckPasswordAsync(user, authUser.Password);
if (!isPassword.Result) return BadRequest(AppResources.UserBadCredentials);
var token = _jwtHandlerAuth.Authentication(authUser);
if (token == null) return BadRequest(AppResources.UserAuthenticationImpossible);
string cookieValue = Request.Cookies["jwt"];
var returnUser = new ReturnUser
{
Email = user.Email,
Name = $"{user.FirstName} {user.LastName}",
UserName = user.UserName
};
if (cookieValue != token)
{
Response.Cookies.Append("jwt", token, new CookieOptions
{
HttpOnly = true,
IsEssential = true,
SameSite = SameSiteMode.None,
Secure = true
});
}
var signInResult = await _signInManager.PasswordSignInAsync(authUser.Email, authUser.Password, false, false);
if (!signInResult.Succeeded) return BadRequest(AppResources.UserAuthenticationImpossible);
var context = _signInManager.Context;
return Ok(returnUser);
}
}
My Authentication method (creates the token):
public string Authentication(AuthUser authUser)
{
var tokenHandler = new JwtSecurityTokenHandler();
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim("UserName", authUser.Email),
new Claim("Email", authUser.Email)
}),
Expires = DateTime.UtcNow.AddHours(2),
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(Encoding.UTF32.GetBytes(_privateKey)),
SecurityAlgorithms.HmacSha256Signature)
};
var claimsPrincipal = new ClaimsPrincipal(tokenDescriptor.Subject);
var token = tokenHandler.CreateToken(tokenDescriptor);
tokenHandler.WriteToken(token);
return tokenHandler.WriteToken(token);
}
And my Startup class services and middleware:
var builder = services.AddIdentityCore<User>();
var identityBuilder = new IdentityBuilder(builder.UserType, builder.Services);
identityBuilder.AddRoles<IdentityRole>();
identityBuilder.AddEntityFrameworkStores<IdentityContext>();
identityBuilder.AddSignInManager<SignInManager<User>>();
identityBuilder.AddDefaultTokenProviders();
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
x.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddCookie("Identity.Application").AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF32.GetBytes(Configuration.GetSection("Jwt:PrivateKey").Value)),
ValidateIssuer = false,
ValidateAudience = false
};
});
services.AddSingleton<IJwtHandlerAuth>(new JwtHandlerAuth(Configuration.GetSection("Jwt:PrivateKey").Value));
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

Add to Slack in dotnetcore without having Identity Framework error: The oauth state was missing or invalid

I'm trying to create a very simple page for my slackbot so that users can login and register. However, even when using their generated "Login with Slack" button I receive an error "The oauth state was missing or invalid.". The same error happens with "Add to Slack".
I based my code off of https://dotnetthoughts.net/slack-authentication-with-aspnet-core/. Even though it's outdated, it's the only example I could find online. I tried figuring out what I need to change in order to get it to work with the dotnetcore 3 and Slack 2.0, but I've come to my wits end.
In my services, I have the following before calling AddMvc, etc.
services.AddAuthentication(options =>
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = "MyAuthCookieName";
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.MaxAge = TimeSpan.FromDays(7);
options.ExpireTimeSpan = TimeSpan.FromDays(7);
options.LoginPath = $"/login";
options.LogoutPath = $"/logout";
options.AccessDeniedPath = $"/AccessDenied";
options.SlidingExpiration = true;
options.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
})
//.AddSlack(options =>
//{
// options.ClientId = Configuration["Slack:ClientId"];
// options.ClientSecret = Configuration["Slack:ClientSecret"];
//});
.AddOAuth("Slack", options =>
{
options.ClientId = Configuration["Slack:ClientId"];
options.ClientSecret = Configuration["Slack:ClientSecret"];
options.CallbackPath = new PathString("/signin-slack");
options.AuthorizationEndpoint = $"https://slack.com/oauth/authorize";
options.TokenEndpoint = "https://slack.com/api/oauth.access";
options.UserInformationEndpoint = "https://slack.com/api/users.identity?token=";
options.Scope.Add("identity.basic");
options.Events = new OAuthEvents()
{
OnCreatingTicket = async context =>
{
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint + context.AccessToken);
var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode();
var userObject = JObject.Parse(await response.Content.ReadAsStringAsync());
var user = userObject.SelectToken("user");
var userId = user.Value<string>("id");
if (!string.IsNullOrEmpty(userId))
{
context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId, ClaimValueTypes.String, context.Options.ClaimsIssuer));
}
var fullName = user.Value<string>("name");
if (!string.IsNullOrEmpty(fullName))
{
context.Identity.AddClaim(new Claim(ClaimTypes.Name, fullName, ClaimValueTypes.String, context.Options.ClaimsIssuer));
}
}
};
});
My configure method looks like
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.Map("/login", builder =>
{
builder.Run(async context =>
{
await context.ChallengeAsync("Slack", properties: new AuthenticationProperties { RedirectUri = "/" });
});
});
app.Map("/logout", builder =>
{
builder.Run(async context =>
{
await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
context.Response.Redirect("/");
});
});
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapRazorPages();
});
Besides the "oauth state was missing on invalid", if in my app I directly go to /login I don't receive the error, but it doesn't appear that I'm logged in as User.Identity.IsAuthenticated is false.
I'm really at a loss, and could use some much appreciated help!
Thank you!
MASSIVE UPDATE
I got the log into slack to work, but I cannot get the Add to Slack button to work.
Here is my new services:
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/login";
options.LogoutPath = "/logout";
})
.AddSlack(options =>
{
options.ClientId = Configuration["Slack:ClientId"];
options.ClientSecret = Configuration["Slack:ClientSecret"];
options.CallbackPath = $"{SlackAuthenticationDefaults.CallbackPath}?state={Guid.NewGuid():N}";
options.ReturnUrlParameter = new PathString("/");
options.Events = new OAuthEvents()
{
OnCreatingTicket = async context =>
{
var request = new HttpRequestMessage(HttpMethod.Get, $"{context.Options.UserInformationEndpoint}?token={context.AccessToken}");
var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode();
var userObject = JObject.Parse(await response.Content.ReadAsStringAsync());
var user = userObject.SelectToken("user");
var userId = user.Value<string>("id");
if (!string.IsNullOrEmpty(userId))
{
context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId, ClaimValueTypes.String, context.Options.ClaimsIssuer));
}
var fullName = user.Value<string>("name");
if (!string.IsNullOrEmpty(fullName))
{
context.Identity.AddClaim(new Claim(ClaimTypes.Name, fullName, ClaimValueTypes.String, context.Options.ClaimsIssuer));
}
}
};
});
Per #timur,I scraped my app.Map and went with an Authentication Controller:
public class AuthenticationController : Controller
{
[HttpGet("~/login")]
public async Task<IActionResult> SignIn()
{
return Challenge(new AuthenticationProperties { RedirectUri = "/" }, "Slack");
}
[HttpGet("~/signin-slack")]
public IActionResult SignInSlack()
{
return RedirectToPage("/Index");
}
[HttpGet("~/logout"), HttpPost("~/logout")]
public IActionResult SignOut()
{
return SignOut(new AuthenticationProperties { RedirectUri = "/" },
CookieAuthenticationDefaults.AuthenticationScheme);
}
}
The "Add to Slack" button is provided as is from Slack.
<img alt="Add to Slack" height="40" width="139" src="https://platform.slack-edge.com/img/add_to_slack.png" srcset="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack#2x.png 2x" />
So, when the use clicks "Login" it logs them in and I get their name, etc. You'll notice in my Authentication Controller I added a function with the path "~/signin-slack" this is because I manually added the "Options.CallbackPath" to add a state parameter. If I remove "Options.CallbackPath", I get an error stating that the oauth state was missing or invalid.
So, I'm not sure what I'm missing here on the Slack side. They make it sound so easy!
Sorry for the long post/update. Thanks for your help.
That same article you mention has a link down below that points to AspNet.Security.OAuth.Providers source repo. That seems to be fairly active, and supports HEAPS of additional oAuth targets including Slack.
I am assuming you've created and configured your slack app. Redirect URL part is of utmost importance there, as it matters whether you specify http or https callback (my example worked only when I went https).
With all above said, I believe the general way to go about implementing it would be to
Install-Package AspNet.Security.OAuth.Slack -Version 3.0.0
and edit your Startup.cs like so:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options => { /* your options verbatim */ })
.AddSlack(options =>
{
options.ClientId = "xxx";
options.ClientSecret = "xxx";
});
}
I see you opted to map your login/logout routes directly in the Startup class, which might actually be the issue - calls to .Map() branch the request pipeline and therefore you don't hit the same middleware chain you set up earlier), so I went with a separate controller (as per sample app):
public class AuthenticationController : Controller
{
[HttpGet("~/signin")]
public async Task<IActionResult> SignIn()
{
// Instruct the middleware corresponding to the requested external identity
// provider to redirect the user agent to its own authorization endpoint.
// Note: the authenticationScheme parameter must match the value configured in Startup.cs
return Challenge(new AuthenticationProperties { RedirectUri = "/" }, "Slack");
}
[HttpGet("~/signout"), HttpPost("~/signout")]
public IActionResult SignOut()
{
// Instruct the cookies middleware to delete the local cookie created
// when the user agent is redirected from the external identity provider
// after a successful authentication flow (e.g Google or Facebook).
return SignOut(new AuthenticationProperties { RedirectUri = "/" },
CookieAuthenticationDefaults.AuthenticationScheme);
}
}
Looking at your snippet I however suspect you already installed this nuget package and tried to use it. Which leads me to recommend a few things to check out:
double check your redirect URL in slack app configuration,
check whether your identity.basic scope is actually enabled for your app
try handling login actions in separate controller rather than startup class
ensure your application runs with SSL: **Project properties** -> **Debug** tab -> **Enable SSL** checkbox (if IIS express hosted, otherwise you might need to do a bit of extra work)
check out the sample project, it might give you an idea how your setup is different
UPD: so after some back and forth I was able to get a better view of your issue. I do believe what you are observing is separate to logging in with slack and rather has to do with their app install flow. As you already pointed out, the difference between the "add to slack" flow and user login is - the state parameter is not part of your source URL and therefore is not returned back to you across requests. This is a huge deal for the oAuth handler as it relies on state to validate request integrity and simply fails if state is empty. There's been a discussion on github but the outcome I believe was - you're going to have to skip the validation part yourself.
So I inherited from SlackAuthenticationHandler that comes with the nuget package and removed the bits of code that gave me the issue:
public class SlackNoStateAuthenticationHandler : SlackAuthenticationHandler {
public SlackNoStateAuthenticationHandler([NotNull] IOptionsMonitor<SlackAuthenticationOptions> options,
[NotNull] ILoggerFactory logger,
[NotNull] UrlEncoder encoder,
[NotNull] ISystemClock clock) : base(options, logger, encoder, clock) { }
public void GenerateCorrelationIdPublic(AuthenticationProperties properties)
{
GenerateCorrelationId(properties);
}
protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync()
{
var query = Request.Query;
var state = query["state"];
var properties = Options.StateDataFormat.Unprotect(state);
var error = query["error"];
if (!StringValues.IsNullOrEmpty(error))
{
// Note: access_denied errors are special protocol errors indicating the user didn't
// approve the authorization demand requested by the remote authorization server.
// Since it's a frequent scenario (that is not caused by incorrect configuration),
// denied errors are handled differently using HandleAccessDeniedErrorAsync().
// Visit https://tools.ietf.org/html/rfc6749#section-4.1.2.1 for more information.
if (StringValues.Equals(error, "access_denied"))
{
return await HandleAccessDeniedErrorAsync(properties);
}
var failureMessage = new StringBuilder();
failureMessage.Append(error);
var errorDescription = query["error_description"];
if (!StringValues.IsNullOrEmpty(errorDescription))
{
failureMessage.Append(";Description=").Append(errorDescription);
}
var errorUri = query["error_uri"];
if (!StringValues.IsNullOrEmpty(errorUri))
{
failureMessage.Append(";Uri=").Append(errorUri);
}
return HandleRequestResult.Fail(failureMessage.ToString(), properties);
}
var code = query["code"];
if (StringValues.IsNullOrEmpty(code))
{
return HandleRequestResult.Fail("Code was not found.", properties);
}
var tokens = await ExchangeCodeAsync(new OAuthCodeExchangeContext(properties, code, BuildRedirectUri(Options.CallbackPath)));
if (tokens.Error != null)
{
return HandleRequestResult.Fail(tokens.Error, properties);
}
if (string.IsNullOrEmpty(tokens.AccessToken))
{
return HandleRequestResult.Fail("Failed to retrieve access token.", properties);
}
var identity = new ClaimsIdentity(ClaimsIssuer);
if (Options.SaveTokens)
{
var authTokens = new List<AuthenticationToken>();
authTokens.Add(new AuthenticationToken { Name = "access_token", Value = tokens.AccessToken });
if (!string.IsNullOrEmpty(tokens.RefreshToken))
{
authTokens.Add(new AuthenticationToken { Name = "refresh_token", Value = tokens.RefreshToken });
}
if (!string.IsNullOrEmpty(tokens.TokenType))
{
authTokens.Add(new AuthenticationToken { Name = "token_type", Value = tokens.TokenType });
}
if (!string.IsNullOrEmpty(tokens.ExpiresIn))
{
int value;
if (int.TryParse(tokens.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out value))
{
// https://www.w3.org/TR/xmlschema-2/#dateTime
// https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx
var expiresAt = Clock.UtcNow + TimeSpan.FromSeconds(value);
authTokens.Add(new AuthenticationToken
{
Name = "expires_at",
Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
});
}
}
properties.StoreTokens(authTokens);
}
var ticket = await CreateTicketAsync(identity, properties, tokens);
if (ticket != null)
{
return HandleRequestResult.Success(ticket);
}
else
{
return HandleRequestResult.Fail("Failed to retrieve user information from remote server.", properties);
}
}
}
Most of this code is verbatim copy of the relevant source, so you could always make more changes if need be;
Then we need to inject the sensible state parameter into your URL. Assuming you've got a controller and a view:
HomeController
public class HomeController : Controller
{
private readonly IAuthenticationHandlerProvider _handler;
public HomeController(IAuthenticationHandlerProvider handler)
{
_handler = handler;
}
public async Task<IActionResult> Index()
{
var handler = await _handler.GetHandlerAsync(HttpContext, "Slack") as SlackNoStateAuthenticationHandler; // we'd get the configured instance
var props = new AuthenticationProperties { RedirectUri = "/" }; // provide some sane defaults
handler.GenerateCorrelationIdPublic(props); // generate xsrf token and add it into the properties object
ViewBag.state = handler.Options.StateDataFormat.Protect(props); // and push it into your view.
return View();
}
}
Startup.cs
.AddOAuth<SlackAuthenticationOptions, SlackNoStateAuthenticationHandler>(SlackAuthenticationDefaults.AuthenticationScheme, SlackAuthenticationDefaults.DisplayName, options =>
{
options.ClientId = "your_id";
options.ClientSecret = "your_secret";
});
Index.cshtml
<img alt="Add to Slack" height="40" width="139" src="https://platform.slack-edge.com/img/add_to_slack.png" srcset="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack#2x.png 2x">
this allowed me to successfully complete the request, although I'm not entirely sure if doing this will be considered best practice
So I figured it out. The login is totally separate from the "Add to Slack" functionality.
So, for logging in I have my services as:
var slackState = Guid.NewGuid().ToString("N");
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/login";
options.LogoutPath = "/logout";
})
.AddSlack(options =>
{
options.ClientId = Configuration["Slack:ClientId"];
options.ClientSecret = Configuration["Slack:ClientSecret"];
options.CallbackPath = $"{SlackAuthenticationDefaults.CallbackPath}?state={slackState}";
options.ReturnUrlParameter = new PathString("/");
options.Events = new OAuthEvents()
{
OnCreatingTicket = async context =>
{
var request = new HttpRequestMessage(HttpMethod.Get, $"{context.Options.UserInformationEndpoint}?token={context.AccessToken}");
var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode();
var userObject = JObject.Parse(await response.Content.ReadAsStringAsync());
var user = userObject.SelectToken("user");
var userId = user.Value<string>("id");
if (!string.IsNullOrEmpty(userId))
{
context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId, ClaimValueTypes.String, context.Options.ClaimsIssuer));
}
var fullName = user.Value<string>("name");
if (!string.IsNullOrEmpty(fullName))
{
context.Identity.AddClaim(new Claim(ClaimTypes.Name, fullName, ClaimValueTypes.String, context.Options.ClaimsIssuer));
}
}
};
});
My AuthenticationController now looks like:
public class AuthenticationController : Controller
{
private readonly ILogger<AuthenticationController> _logger;
private readonly AppSettings _appSettings;
public AuthenticationController(ILogger<AuthenticationController> logger, IOptionsMonitor<AppSettings> appSettings)
{
_logger = logger;
_appSettings = appSettings.CurrentValue;
}
[HttpGet("~/login")]
public IActionResult SignIn()
{
return Challenge(new AuthenticationProperties { RedirectUri = "/" }, "Slack");
}
[HttpGet("~/signin-slack")]
public async Task<IActionResult> SignInSlack()
{
var clientId = _appSettings.Slack.ClientId;
var clientSecret = _appSettings.Slack.ClientSecret;
var code = Request.Query["code"];
SlackAuthRequest slackAuthRequest;
string responseMessage;
var requestUrl = $"https://slack.com/api/oauth.access?client_id={clientId}&client_secret={clientSecret}&code={code}";
var request = new HttpRequestMessage(HttpMethod.Post, requestUrl);
using (var client = new HttpClient())
{
var response = await client.SendAsync(request).ConfigureAwait(false);
var result = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
slackAuthRequest = JsonConvert.DeserializeObject<SlackAuthRequest>(result);
}
if (slackAuthRequest != null)
{
_logger.LogInformation("New installation of StanLeeBot for {TeamName} in {Channel}", slackAuthRequest.TeamName, slackAuthRequest.IncomingWebhook.Channel);
var webhookUrl = slackAuthRequest.IncomingWebhook.Url;
var sbmClient = new SbmClient(webhookUrl);
var message = new Message
{
Text = "Hi there from StanLeeBot!"
};
await sbmClient.SendAsync(message).ConfigureAwait(false);
responseMessage = $"Congrats! StanLeeBot has been successfully added to {slackAuthRequest.TeamName} {slackAuthRequest.IncomingWebhook.Channel}";
return RedirectToPage("/Index", new { message = responseMessage });
}
_logger.LogError("Something went wrong making a request to {RequestUrl}", requestUrl);
responseMessage = "Error: Something went wrong and we were unable to add StanLeeBot to your Slack.";
return RedirectToPage("/Index", new { message = responseMessage });
}
[HttpGet("~/logout"), HttpPost("~/logout")]
public IActionResult SignOut()
{
return SignOut(new AuthenticationProperties { RedirectUri = "/" },
CookieAuthenticationDefaults.AuthenticationScheme);
}
}
SmbClient is a Nuget package called SlackBotMessages that is used to send messages. So after the user authenticates, a message is automatically sent to that channel welcoming the user.
Thank you all very much for your help! Let me know what you think or if you see any gotchas.

OpenIdDict logout automatic so fast

I use openiddict in netcore 2 project. the problem is after some request user be logout and i cant fix that by increase login time.
Here is my code to add openiddict
services.AddOpenIddict()
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<DataDbContext>()
.ReplaceDefaultEntities<Guid>();
})
.AddServer(options =>
{
options.UseMvc();
options.EnableTokenEndpoint("/api/token");
options.AllowPasswordFlow();
options.AcceptAnonymousClients();
options.DisableHttpsRequirement();
options.SetAccessTokenLifetime(TimeSpan.FromDays(10));
options.SetIdentityTokenLifetime(TimeSpan.FromDays(10));
})
.AddValidation();
services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = OpenIdConnectConstants.Claims.Name;
options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject;
options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role;
options.Password.RequireDigit = false;
options.Password.RequiredLength = 4;
options.Password.RequireLowercase = false;
options.Password.RequireUppercase = false;
options.Password.RequiredUniqueChars = 0;
options.Password.RequireNonAlphanumeric = false;
});
services.AddAuthentication(options =>
{
options.DefaultScheme = OpenIddictValidationDefaults.AuthenticationScheme;
});
after create token
var ticket =await CreateTicketAsync(request, user);
ticket.SetAccessTokenLifetime(TimeSpan.FromDays(7)); var tocken =
SignIn(ticket.Principal, ticket.Properties,
ticket.AuthenticationScheme);
i used it
private async Task<AuthenticationTicket> CreateTicketAsync(OpenIdConnectRequest request, ApplicationUser user)
{
// Create a new ClaimsPrincipal containing the claims that
// will be used to create an id_token, a token or a code.
var principal = await _signInManager.CreateUserPrincipalAsync(user);
// Create a new authentication ticket holding the user identity.
var ticket = new AuthenticationTicket(principal,
new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
// ticket.Properties.ExpiresUtc = DateTime.Now.AddDays(2);
// Set the list of scopes granted to the client application.
ticket.SetScopes(new[]
{
OpenIdConnectConstants.Scopes.OpenId,
//OpenIdConnectConstants.Scopes.Email,
OpenIdConnectConstants.Scopes.Profile,
OpenIddictConstants.Scopes.Roles
}.Intersect(request.GetScopes()));
ticket.SetResources("resource-server");
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
foreach (var claim in ticket.Principal.Claims)
{
// Never include the security stamp in the access and identity tokens, as it's a secret value.
if (claim.Type == _identityOptions.Value.ClaimsIdentity.SecurityStampClaimType)
{
continue;
}
var destinations = new List<string>
{
OpenIdConnectConstants.Destinations.AccessToken
};
// Only add the iterated claim to the id_token if the corresponding scope was granted to the client application.
// The other claims will only be added to the access_token, which is encrypted when using the default format.
if ((claim.Type == OpenIdConnectConstants.Claims.Name && ticket.HasScope(OpenIdConnectConstants.Scopes.Profile)) ||
(claim.Type == OpenIdConnectConstants.Claims.Email && ticket.HasScope(OpenIdConnectConstants.Scopes.Email)) ||
(claim.Type == OpenIdConnectConstants.Claims.Role && ticket.HasScope(OpenIddictConstants.Claims.Roles)))
{
destinations.Add(OpenIdConnectConstants.Destinations.IdentityToken);
}
claim.SetDestinations(destinations);
}
return ticket;
}

System.Security.Claims.ClaimType Role doesn't match

Just trying to understand a problem, that appears in my head to be a bug in the Microsoft System.Security dll, but probably something I am doing incorrectly.
I am trying to custom implement a JWT token. I create the claims and the token:
var claimsIdentity = new ClaimsIdentity(new List<System.Security.Claims.Claim>()
{
new System.Security.Claims.Claim(ClaimTypes.Sid, "1"),
new System.Security.Claims.Claim(ClaimTypes.Role, "1"),
},"Custom");
var securityTokenDescriptor = new SecurityTokenDescriptor()
{
AppliesToAddress = Keys.Core.WebsiteDomain,
TokenIssuerName = Keys.Core.WebsiteDomain,
Subject = claimsIdentity,
SigningCredentials = signingCredentials,
};
var tokenHandler = new JwtSecurityTokenHandler();
var plainToken = tokenHandler.CreateToken(securityTokenDescriptor);
var signedAndEncodedToken = tokenHandler.WriteToken(plainToken);
return signedAndEncodedToken;
I then go to retrieve the token and the User (SID) and Role values:
var roleId = stream.Claims.SingleOrDefault(x => x.Type == ClaimTypes.Role).Value;
var userId = stream.Claims.SingleOrDefault(x => x.Type == ClaimTypes.Sid).Value;
Validate Function:
private static JwtSecurityToken Validate(string signedAndEncodedToken)
{
var tokenHandler = new CustomJwtSecurityTokenHandler();
var plainTextSecurityKey = Keys.Security.TokenSecret;
var signingKey = new InMemorySymmetricSecurityKey(
Encoding.UTF8.GetBytes(plainTextSecurityKey));
var tokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = false,
ValidateAudience = false,
IssuerSigningKey = signingKey
};
SecurityToken validatedToken;
tokenHandler.ValidateToken(signedAndEncodedToken, tokenValidationParameters, out validatedToken);
var jwtToken = validatedToken as JwtSecurityToken;
return validatedToken as JwtSecurityToken;
}
Now My UserID (ClaimType.SID) appears to return correctly, but my RoleId (ClaimType.Role) comes back as non existant. If I change x.Type == "role" it works correctly.
On inspection on the Claim.Type SID appears as: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/sid
But Claim.Type Role appears as: Role.
ClaimTypes.Role has the full Schema path.
Is this a bug, or am I missing something?
I realized sometime has passed, but as someone who was surprised by this as well, I did encounter this comment on the JwtSecurityToken.Claims property:
Claim(s) returned will NOT have the Claim.Type translated according to JwtSecurityTokenHandler.InboundClaimTypeMap
So if you want to use ClaimTypes when searching in a decoded token, you can just run it through that dictionary and know that you're not insane or did anything wrong.
Assert.Equal(
"Nicholas Piasecki",
parsedToken
.Claims
.Single(x =>
{
var map = JwtSecurityTokenHandler.DefaultInboundClaimTypeMap;
if (map.TryGetValue(x.Type, out var mapped))
{
return mapped == ClaimTypes.GivenName;
}
return false;
})
.Value);

Save tokens in Cookie with ASP.NET Core Identity

I want to save something inside my 'Identity' generated cookie. I'm currently using the default Identity setup from the Docs.
Startup.cs
services.Configure<IdentityOptions>(options =>
{
// User settings
options.User.RequireUniqueEmail = true;
// Cookie settings
options.Cookies.ApplicationCookie.AuthenticationScheme = "Cookies";
options.Cookies.ApplicationCookie.ExpireTimeSpan = TimeSpan.FromHours(1);
options.Cookies.ApplicationCookie.SlidingExpiration = true;
options.Cookies.ApplicationCookie.AutomaticAuthenticate = true;
options.Cookies.ApplicationCookie.LoginPath = "/Account";
options.Cookies.ApplicationCookie.LogoutPath = "/Account/Logout";
});
AccountController.cs
var result = await _signInManager.PasswordSignInAsync(user.UserName, model.Password, true, true);
if (result.Succeeded)
{
_logger.LogInformation(1, "User logged in.");
var tokens = new List<AuthenticationToken>
{
new AuthenticationToken {Name = "Test", Value = "Test"},
};
var info = await HttpContext.Authentication.GetAuthenticateInfoAsync("Cookies");
info.Properties.StoreTokens(tokens);
It seems this doesn't work. Because the cookie isn't created yet. The 'Info' variable is empty.
I could solve it by using the 'CookieMiddleware'
Startup.cs
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = "Cookies",
ExpireTimeSpan = TimeSpan.FromHours(1),
SlidingExpiration = true,
AutomaticAuthenticate = true,
LoginPath = "/Account",
LogoutPath = "/Account/Logout",
});
But than I need to use
await HttpContext.Authentication.SignInAsync("Cookies", <userPrincipal>);
In this case I need to build myself a 'user principal'. And I prefer to leverage 'Identity' for this matter.
So is it possible to combine this?
If this is not the case how do I generate the claimsprincipal on a good way.
Without the need to 'map' every claim.
List<Claim> userClaims = new List<Claim>
{
new Claim("UserId", Convert.ToString(user.Id)),
new Claim(ClaimTypes.Name, user.UserName),
// TODO: Foreach over roles
};
ClaimsPrincipal principal = new ClaimsPrincipal(new ClaimsIdentity(userClaims));
await HttpContext.Authentication.SignInAsync("Cookies", principal);
So something like:
ClaimsPrincipal pricipal = new ClaimsPrincipal(user.Claims);
This doesn't work because user.Claims is of type IdentityUserClaim and not of type Security.Claims.Claim.
Thanks for reading.
Have a good day,
Sincerely, Brecht
I managed to solve my problem.
I wrote the same functionality that is inside the 'signInManager'. But adding my own authentication property.
var result = await _signInManager.PasswordSignInAsync(user, model.Password, true, true);
if (result.Succeeded)
{
await AddTokensToCookie(user, model.Password);
return RedirectToLocal(returnUrl);
}
if (result.RequiresTwoFactor)
{
// Ommitted
}
if (result.IsLockedOut)
{
// Ommitted
}
Code that actually saves something (tokens) inside the cookie:
private async Task AddTokensToCookie(ApplicationUser user, string password)
{
// Retrieve access_token & refresh_token
var disco = await DiscoveryClient.GetAsync(Environment.GetEnvironmentVariable("AUTHORITY_SERVER") ?? "http://localhost:5000");
if (disco.IsError)
{
_logger.LogError(disco.Error);
throw disco.Exception;
}
var tokenClient = new TokenClient(disco.TokenEndpoint, "client", "secret");
var tokenResponse = await tokenClient.RequestResourceOwnerPasswordAsync(user.Email, password, "offline_access api1");
var tokens = new List<AuthenticationToken>
{
new AuthenticationToken {Name = OpenIdConnectParameterNames.AccessToken, Value = tokenResponse.AccessToken},
new AuthenticationToken {Name = OpenIdConnectParameterNames.RefreshToken, Value = tokenResponse.RefreshToken}
};
var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(tokenResponse.ExpiresIn);
tokens.Add(new AuthenticationToken
{
Name = "expires_at",
Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
});
// Store tokens in cookie
var prop = new AuthenticationProperties();
prop.StoreTokens(tokens);
prop.IsPersistent = true; // Remember me
await _signInManager.SignInAsync(user, prop);
}
The last 4 lines of code are the most important ones.

Categories