This time I'm triying to set and get the whole information about the user at the FrontSide but I don't know whant i'm doing wrong
I have two separated projects the first one is the Webapi Project and I'm using it to SingIn the user giving then a token.
// GET api/Account/ExternalLogin
[OverrideAuthentication]
[HostAuthentication(DefaultAuthenticationTypes.ExternalCookie)]
[AllowAnonymous]
[Route("ExternalLogin", Name = "ExternalLogin")]
public async Task<IHttpActionResult> GetExternalLogin(string provider, string error = null)
{
if (error != null)
return Redirect(Url.Content("~/") + "#error=" + Uri.EscapeDataString(error));
if (!User.Identity.IsAuthenticated)
return new ChallengeResult(provider, this);
ExternalLoginData externalLogin = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity);
if (externalLogin == null)
return InternalServerError();
if (externalLogin.LoginProvider != provider)
{
Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
return new ChallengeResult(provider, this);
}
AppJobSeeker user = await UserManager.FindAsync(new UserLoginInfo(externalLogin.LoginProvider, externalLogin.ProviderKey));
bool hasRegistered = user != null;
if (hasRegistered)
{
Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
ClaimsIdentity oAuthIdentity = await UserManager.CreateIdentityAsync(user, OAuthDefaults.AuthenticationType);
ClaimsIdentity cookieIdentity = await UserManager.CreateIdentityAsync(user, CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = ApplicationOAuthProvider.CreateProperties(user.UserName, user.Id);
Authentication.SignIn(properties, oAuthIdentity, cookieIdentity);
}
else
{
IEnumerable<Claim> claims = externalLogin.GetClaims();
ClaimsIdentity identity = new ClaimsIdentity(claims, OAuthDefaults.AuthenticationType);
Authentication.SignIn(identity);
}
return Ok();
}
And the client side is a MVC 5 Project where I have one method to postasyn the authentication and another one to Create the AuthTickect like this...
public async Task<T> AuthenticateAsync<T>(string userName, string password)
{
using (var client = new HttpClient())
{
var result = await client.PostAsync((#"http://localhost:8060/Token"), new FormUrlEncodedContent(new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>(#"grant_type", #"password"),
new KeyValuePair<string, string>(#"userName", userName),
new KeyValuePair<string, string>(#"password", password)
}));
string json = await result.Content.ReadAsStringAsync();
if (result.IsSuccessStatusCode)
return JsonConvert.DeserializeObject<T>(json);
throw new ApiException(result.StatusCode, json);
}
}
private void CreateTicket(SignInResult result, SignInModel model, string returnUrl)
{
//Let's keep the user authenticated in the MVC webapp.
//By using the AccessToken, we can use User.Identity.Name in the MVC controllers to make API calls.
FormsAuthentication.SetAuthCookie(result.AccessToken, model.RememberMe);
//Create an AuthenticationTicket to generate a cookie used to authenticate against Web API.
//But before we can do that, we need a ClaimsIdentity that can be authenticated in Web API.
Claim[] claims =
{
new Claim(ClaimTypes.Name, result.AccessToken), //Name is the default name claim type, and UserName is the one known also in Web API.
new Claim(ClaimTypes.Email, result.UserName), //If you want to use User.Identity.GetUserId in Web API, you need a NameIdentifier claim.
};
//Generate a new ClaimsIdentity, using the DefaultAuthenticationTypes.ApplicationCookie authenticationType.
//This also matches what we've set up in Web API.
AuthenticationTicket authTicket = new AuthenticationTicket(new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie), new AuthenticationProperties
{
ExpiresUtc = result.Expires,
IsPersistent = model.RememberMe,
IssuedUtc = result.Issued,
RedirectUri = returnUrl,
});
//HttpContext.Response..User = principal;
//And now it's time to generate the cookie data. This is using the same code that is being used by the CookieAuthenticationMiddleware class in OWIN.
byte[] userData = DataSerializers.Ticket.Serialize(authTicket);
//Protect this user data and add the extra properties. These need to be the same as in Web API!
byte[] protectedData = MachineKey.Protect(userData, new[] { "Microsoft.Owin.Security.Cookies.CookieAuthenticationMiddleware", DefaultAuthenticationTypes.ApplicationCookie, "v1" });
//base64-encode this data.
string protectedText = TextEncodings.Base64Url.Encode(protectedData);
//And now, we have the cookie.
Response.SetCookie(new HttpCookie("JobSeekerAuth")
{
HttpOnly = true,
Expires = result.Expires.UtcDateTime,
Value = protectedText,
});
}
And my login method looks like
// POST: Account/SignIn
[HttpPost]
public async Task<ActionResult> Login(SignInModel model, string returnUrl)
{
if (!ModelState.IsValid)
return View(model);
try
{
CreateTicket(await WebApiService.Instance.AuthenticateAsync<SignInResult>(model.Email, model.Password), model, returnUrl);
return RedirectToLocal(returnUrl);
//return await WebApiService.Instance.AuthenticateAsync<SignInResult>(model.Email, model.Password) != null ? RedirectToLocal(returnUrl) : RedirectToLocal(returnUrl);
}
catch (ApiException ex)
{
//No 200 OK result, what went wrong?
HandleBadRequest(ex);
if (!ModelState.IsValid)
return View(model);
throw;
}
}
The problem is I want to use the GenericPrincipal at the Razor View two get the userId or the username fro the logged user and when I trying to doing so It gives me nothing more than the token here
#if (HttpContext.Current.User.Identity.IsAuthenticated)
{
<li>#Html.ActionLink("Sign Out", "SignOut", "Account")</li>
}
else
{...
So, I don't know how to get this goal
Best regards!...
My Authentication Method Works Well because once I go to the login method this one gives me my entity SignInResult wich looks like with all its value setted
[JsonProperty("access_token")]
public string AccessToken { get; set; }
//Included to show all the available properties, but unused in this sample
[JsonProperty("token_type")]
public string TokenType { get; set; }
[JsonProperty("expires_in")]
public uint ExpiresIn { get; set; }
[JsonProperty("userName")]
public string UserName { get; set; }
[JsonProperty(".issued")]
public DateTimeOffset Issued { get; set; }
[JsonProperty(".expires")]
public DateTimeOffset Expires { get; set; }
[JsonProperty("userId")]
public string UserId { get; set; }
I also tried to set to the Thread.CurrentPrincipal but not success
Best Regards
Related
I'm trying to learn how to use authentication and SignInAsync via an API. For some reason, my code works only if I call the API from Postman or from Swagger, but not when I call it from code in the front-end.
To test this, I created two small projects: a web razor pages front end and a simple API. The API has the 3 possible POST requests (TestLogin, TestLogout and TestVerify) as show below:
[Route("api/[controller]")]
[ApiController]
[Authorize()]
public class LoginTestController : ControllerBase
{
[AllowAnonymous]
[HttpPost("TestLogin")]
public async Task<ActionResult> TestLoginAction([FromBody] LoginData loginData)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, loginData.UserName),
new Claim(ClaimTypes.Role, loginData.UserRole)
};
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
if (principal is not null && principal.Identity is not null)
{
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
return Ok("Successfully logged in.");
}
else
{
return BadRequest("Login failed.");
}
}
[HttpPost("TestVerify")]
public ActionResult TestVerify()
{
var user = User.Identity;
if (user is not null && user.IsAuthenticated)
return Ok($"Logged in user: " + user.Name);
else
return Ok("No user logged in. Please login.");
}
[HttpPost("TestLogout")]
public async Task<ActionResult> TestLogout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Ok();
}
}
In my Program.cs, I add the cookie authentication
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie();
and
app.UseAuthentication();
app.UseAuthorization();
In my front-end razor page, I called the APIs this way using this code:
public string Message { get; set; }
public async Task OnPostLogin()
{
var client = new HttpClient();
var userLoginInfo = new LoginData
{
UserName = "TestUser",
UserRole = "TestRole"
};
string api = "https://localhost:7049/api/LoginTest/TestLogin";
var response = await client.PostAsJsonAsync(api, userLoginInfo);
Message = await response.Content.ReadAsStringAsync();
}
public async Task OnPostLogout()
{
var client = new HttpClient();
var userLoginInfo = new LoginData();
string api = "https://localhost:7049/api/LoginTest/TestLogout";
var response = await client.PostAsJsonAsync(api, userLoginInfo);
Message = await response.Content.ReadAsStringAsync();
}
public async Task OnPostValidate()
{
var client = new HttpClient();
var userLoginInfo = new LoginData();
string api = "https://localhost:7049/api/LoginTest/TestVerify";
var response = await client.PostAsJsonAsync(api, userLoginInfo);
int statusCode = ((int)response.StatusCode);
if (response.IsSuccessStatusCode)
Message = await response.Content.ReadAsStringAsync();
else
Message = $"Failed with status code: ({response.StatusCode}/{statusCode})";
}
If I try via the front-end, I click on the Login, and it says it is successfully, but when I click on the Validate, it returns a 404 presumably because it doesn't think it is authorized even though I did a log in (or if I authorize anonymous for the validate, it says no one is logged in). If I do the same calls via Swagger, the login works the same, but the validate remembers the login, which is the desired behavior. Why is the login not being remembered when I call the API via the front-end, but it is when I do it via Swagger? And how can I fix this?
Thanks.
I currently have and application that is using Identity to authorize users. I need to change it to use Azure AD to login. After being authenticated through azure I need to use the information of the logged in user that we have in the identity database. After the user is authenticated I get a
NullReferenceException: Object reference not set to an instance of an object.
and fails at this point:
ApplicationUser user = await manager.FindByNameAsync(context.Principal.Identity.Name);
```
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication().AddOpenIdConnect(c =>
{
c.Authority = "https://login.microsoftonline.com/common";
c.ClientId = "<insert-registered-guid>";
c.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false
};
c.Events.OnTokenValidated = async context =>
{
UserManager<ApplicationUser> manager = context.HttpContext.RequestServices.GetService<UserManager<ApplicationUser>>();
SignInManager<ApplicationUser> signIn = context.HttpContext.RequestServices.GetService<SignInManager<ApplicationUser>>();
ApplicationUser user = await manager.FindByNameAsync(context.Principal.Identity.Name);
if (user != null)
{
await signIn.SignInAsync(user, false);
}
};
});
}
// HomeController.cs
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
public class HomeController : Controller
{
[AllowAnonymous]
public IActionResult LoginWithAzure()
{
string redirectUrl = Url.Content("~/");
return Challenge(new AuthenticationProperties { RedirectUri = redirectUrl }, OpenIdConnectDefaults.AuthenticationScheme);
}
}
```
UPDATE:
I was able to get past the error because I was missing
services.AddIdentity
Now the issue is that it gets stuck in a loop inside the OnTokenValidated.
UserManager<ApplicationUser> manager = context.HttpContext.RequestServices.GetService<UserManager<ApplicationUser>>();
SignInManager<ApplicationUser> signIn = context.HttpContext.RequestServices.GetService<SignInManager<ApplicationUser>>();
ApplicationUser user = await manager.FindByNameAsync(context.Principal.Identity.Name);
if (user != null)
{
await signIn.SignInAsync(user, false);
}
after the if statement it goes back to the manager line.
The above solution was not working so I changed it.
Startup.cs was changed to the following:
// Add Azure AD authentication
services.AddAuthentication(defaultScheme: AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
AccountController.cs was changed to this:
[AllowAnonymous]
[HttpGet]
public ChallengeResult InternalSignIn(string returnUrl = "/")
{
var redirectUrl = Url.Action(nameof(ExternalLoginCallback));
var properties = signInManager.ConfigureExternalAuthenticationProperties(AzureADDefaults.AuthenticationScheme, redirectUrl);
return new ChallengeResult(AzureADDefaults.AuthenticationScheme, properties);
}
[HttpGet]
public async Task<IActionResult> ExternalLoginCallback()
{
var info = await signInManager.GetExternalLoginInfoAsync();
if (info is null)
{
return BadRequest();
}
var signInResult = await signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: false);
var email = info.Principal.FindFirstValue(ClaimTypes.Name);
var user = await userManager.FindByEmailAsync(email);
IdentityResult result;
if (user != null)
{
var logins = await userManager.GetLoginsAsync(user);
if (!logins.Any())
{
result = await userManager.AddLoginAsync(user, info);
if (!result.Succeeded)
{
return View();
}
}
await signInManager.SignInAsync(user, isPersistent: false);
return RedirectToAction(nameof(HomeController.Index),"Home");
}
return StatusCode(500, "Internal server error");
}
If user is authenticated via google, I need to get his profile picture.
If user is authenticated via facebook I get his profile picture with this code:
var info = await _signInManager.GetExternalLoginInfoAsync();
var identifier = info.Principal.FindFirstValue(ClaimTypes.NameIdentifier);
var picture = $"https://graph.facebook.com/{identifier}/picture"; // 3
So, which code I need to use in 3 line for getting user's profile picture in case user is authenticated via google?
Accessing user profile picture, full solution.
Stack:
Asp Net Identity 4 v4 + Asp Net Identity + Google People Api
Steps
1. Preparation
1.1 I'm using default HttpClient to generate HTTP requests to Google API
1.2 Install NUGET System.Net.Http.Json to ease serialization.
1.3 Use a [json to c#] tool, to create a DTO like this:
/// <summary>
/// 🟡 official docs.
/// https://developers.google.com/people/api/rest/v1/people#Person.Photo
/// </summary>
public class PeopleApiPhotos {
public string resourceName { get; set; }
public string etag { get; set; }
public List<Photo> photos { get; set; }
public class Source {
public string type { get; set; }
public string id { get; set; }
}
public class Metadata {
public bool primary { get; set; }
public Source source { get; set; }
}
public class Photo {
public Metadata metadata { get; set; }
public string url { get; set; }
}
}
2. Identity Server: when users confirms external info data, send http get request, to retrieve avatar picture url:
Identity Server with Asp Net Identity > Login > OnPostConfirmationAsync: FULL METHOD
public async Task < IActionResult > OnPostConfirmationAsync(string returnUrl = null) {
returnUrl = returnUrl ? ?Url.Content("~/");
// Get the information about the user from the external login provider
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null) {
ErrorMessage = "Error loading external login information during confirmation.";
return RedirectToPage("./Login", new {
ReturnUrl = returnUrl
});
}
// try to get profile picture
string pictureUri = string.Empty;
if (info.LoginProvider.ToLower() == "google") {
var httpClient = _httpClientFactory.CreateClient();
string peopleApiKey = _configuration["GoogleApiKey:PeopleApiKey"];;
var googleAccountId = info.Principal.FindFirstValue(ClaimTypes.NameIdentifier);
var photosResponse = await httpClient.GetFromJsonAsync < PeopleApiPhotos > (
$ "https://people.googleapis.com/v1/people/{googleAccountId}?personFields=photos&key={peopleApiKey}");
pictureUri = photosResponse ? .photos.FirstOrDefault() ? .url;
}
if (ModelState.IsValid) {
// Cria usuário
var user = new AppUser {
UserName = Input.Email,
Email = Input.Email,
FirstName = Input.FirstName,
LastName = Input.LastName,
ProfilePictureUrl = pictureUri
};
var result = await _userManager.CreateAsync(user);
if (result.Succeeded) {
result = await _userManager.AddLoginAsync(user, info);
if (result.Succeeded) {
_logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider);
var userId = await _userManager.GetUserIdAsync(user);
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = Url.Page("/Account/ConfirmEmail", pageHandler: null, values: new {
area = "Identity",
userId = userId,
code = code
},
protocol: Request.Scheme);
await _emailSender.SendEmailAsync(Input.Email, "Confirm your email", $ "Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
// If account confirmation is required, we need to show the link if we don't have a real email sender
if (_userManager.Options.SignIn.RequireConfirmedAccount) {
return RedirectToPage("./RegisterConfirmation", new {
Email = Input.Email
});
}
await _signInManager.SignInAsync(user, isPersistent: false, info.LoginProvider);
return LocalRedirect(returnUrl);
}
}
foreach(var error in result.Errors) {
ModelState.AddModelError(string.Empty, error.Description);
}
}
ProviderDisplayName = info.ProviderDisplayName;
ReturnUrl = returnUrl;
return Page();
}
2.1 Identity Server with Asp Net Identity > Login > OnPostConfirmationAsync: HIGHLIGHT:
❗️ this is only a little part from full method above.
Checks if external provider is Google, and use [NameIdentifier] and [Google Api Key] to reach People Endpoint.
// try to get profile picture
string pictureUri = string.Empty;
if (info.LoginProvider.ToLower() == "google") {
var httpClient = _httpClientFactory.CreateClient();
// ApiKey can get generated in [Google Developers Console](https://console.developers.google.com/apis/credentials).
string peopleApiKey = _configuration["GoogleApiKey:PeopleApiKey"];;
var googleAccountId = info.Principal.FindFirstValue(ClaimTypes.NameIdentifier);
var photosResponse = await httpClient.GetFromJsonAsync < PeopleApiPhotos > (
$ "https://people.googleapis.com/v1/people/{googleAccountId}?personFields=photos&key={peopleApiKey}");
pictureUri = photosResponse ? .photos.FirstOrDefault() ? .url;
}
3. Identity Server > Profile Service Implementation:
public async Task GetProfileDataAsync(ProfileDataRequestContext context) {
var sub = context.Subject.GetSubjectId();
var user = await _userManager.FindByIdAsync(sub);
var principal = await _claimsFactory.CreateAsync(user);
var claims = principal.Claims.ToList();
claims.Add(new Claim(JwtClaimTypes.GivenName, user.FirstName));
claims.Add(new Claim(JwtClaimTypes.FamilyName, user.LastName));
// Insert a new claim, that gets ProfilePictureUrl persisted in my app user in database.
claims.Add(new Claim(JwtClaimTypes.Picture, user.ProfilePictureUrl));
context.IssuedClaims = claims;
}
4. Acessing profile user in ReactJS Frontend:
To get user profile data in frontend, i'm using OidcClient-js
// here i'm using oidc-client.js
// user object is loaded with user full data.
let profilePictureUrl = user.profile.picture;
Thanks to #DaImTo response.
People.get method returns a person object which contains
Your user needs to be authenticated with the profile scope.
Raw http request
GET https://people.googleapis.com/v1/people/me?personFields=photos HTTP/1.1
Authorization: Bearer [YOUR_ACCESS_TOKEN]
Accept: application/json
response
{
"resourceName": "people/117200475532672775346",
"etag": "%EgQBAzcuGgQBAgUHIgxHcHNCRHZycjVkZz0=",
"photos": [
{
"metadata": {
"primary": true,
"source": {
"type": "PROFILE",
"id": "1172004755672775346"
}
},
"url": "https://lh3.googleusercontent.com/a-/AOh14GhroCYJp2P9xeYeYk1npchBPK-zbtTxzNQo0WAHI20=s100"
},
{
"metadata": {
"source": {
"type": "CONTACT",
"id": "3faa96eb0baa4be"
}
},
"url": "https://lh6.googleusercontent.com/-vuhaM1mUvwE/VFOBzFDW-TI/AAAAAAAAAAA/izR9rgfDIyoVoHd7Mq_OJmdbwjhEnfhEQCOQCEAE/s100/photo.jpg"
}
]
}
Note: You can also get this information from the userinfo endpoint however Google does not guarantee that they will send the claims everytime you make the request so IMO its best to go though the people api.
In my application, the user would login into the application and then authorize facebook(by clicking a button in a secure area) to allow the application to get facebook posts etc.
Here's the code that I have written to save the claims and authentication tokens and connect those to the internal user account.
Am I following the right approach? If not, would it be easier to directly insert records into the AspNetUserClaims, AspNetUserTokens tables for claims, authentication tokens, using Entity Framework core.
public async Task<IActionResult> OnGetCallbackAsync(string returnUrl = null, string remoteError = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
if (remoteError != null)
{
ErrorMessage = $"Error from external provider: {remoteError}";
return RedirectToPage("./Settings", new { ReturnUrl = returnUrl });
}
// Get the information about the user from the external login provider
var identityUser = await UserManager.GetUserAsync(User);
var externalLoginInfo = await SignInManager.GetExternalLoginInfoAsync();
if (externalLoginInfo == null)
{
ErrorMessage = "Error loading external login information during confirmation.";
return RedirectToPage("./Settings", new { ReturnUrl = returnUrl });
}
var applicationUser = new ApplicationUser { UserName = identityUser.Email, Email = identityUser.Email , SecurityStamp= Guid.NewGuid().ToString()};
var identityResult = await this.UserManager.AddLoginAsync(applicationUser, externalLoginInfo);
if (identityResult.Succeeded)
{
if (externalLoginInfo.Principal.HasClaim(c => c.Type == ClaimTypes.GivenName))
{
await this.UserManager.AddClaimAsync(applicationUser, externalLoginInfo.Principal.FindFirst(ClaimTypes.GivenName));
}
identityResult = await SignInManager.UpdateExternalAuthenticationTokensAsync(externalLoginInfo);
var authenticationProperties = new AuthenticationProperties();
authenticationProperties.StoreTokens(externalLoginInfo.AuthenticationTokens);
//authenticationProperties.IsPersistent = true;
#endregion
return LocalRedirect(returnUrl);
}
foreach (var error in identityResult.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
LoginProvider = externalLoginInfo.LoginProvider;
ReturnUrl = returnUrl;
return Page();
}
#endregion
}
How to manually generate access_token from server without password?
I want to allow super admins login as users and look at their problems and see the problems by their eyes, so i need user access_token. i already see this question but didn't help me in IdentityServer3.
first create a custom grant named loginBy
public class LoginByGrant : ICustomGrantValidator
{
private readonly ApplicationUserManager _userManager;
public string GrantType => "loginBy";
public LoginByGrant(ApplicationUserManager userManager)
{
_userManager = userManager;
}
public async Task<CustomGrantValidationResult> ValidateAsync(ValidatedTokenRequest request)
{
var userId = Guid.Parse(request.Raw.Get("user_id"));
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
return await Task.FromResult<CustomGrantValidationResult>(new CustomGrantValidationResult("user not exist"));
var userClaims = await _userManager.GetClaimsAsync(user.Id);
return
await Task.FromResult<CustomGrantValidationResult>(new CustomGrantValidationResult(user.Id.ToString(), "custom", userClaims));
}
}
then add this custom grant in identity startup class
factory.CustomGrantValidators.Add(
new Registration<ICustomGrantValidator>(resolver => new LoginByGrant(ApplicaionUserManager)));
and finally in your api
public async Task<IHttpActionResult> LoginBy(Guid userId)
{
var tokenClient = new TokenClient(Constants.TokenEndPoint, Constants.ClientId, Constants.Secret);
var payload = new { user_id = userId.ToString() };
var result = await tokenClient.RequestCustomGrantAsync("loginBy", "customScope", payload);
if (result.IsError)
return Ok(result.Json);
return Ok(new { access_token = result.AccessToken, expires_in = result.ExpiresIn});
}
this is for identityServer3 but for identityServer4 it is pretty similar