I am making a single sign on solution for multiple products and need to be able to log the user into their google account so that they can use other google services like drive and email etc. I am getting access tokens and storing them using the Google.Apis.Auth.OAuth2.Mvc and sample code but I am unsure how to use this token to log the user in. I don't need api access just need to ensure that after the user has gone through the flow the next time they visit my site they are logged into their google account. This is a .net mvc project.
public async Task<ActionResult> IndexAsync(CancellationToken cancellationToken)
{
var result = await new AuthorizationCodeMvcApp(this, flowData).AuthorizeAsync(cancellationToken);
if (result == null || result.Credential == null)
return new RedirectResult(result.RedirectUri);
return new RedirectResult(Request.UrlReferrer.AbsolutePath);
}
initiater = new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = "30725746816-ev4dn6bt84cmj8ksemsrh894lcngh45q.apps.googleusercontent.com",
ClientSecret = "-Sm8vvVjlIsR8M4AxMR8-MZw",
},
Scopes = new[] { "openid profile email" },
DataStore = new GoogleIDataStore(_session, user),
};
currentUser = user;
flow = new GoogleAuthorizationCodeFlow(initiater);
Related
On ASP.net CORE 3, when a user logout, I would like to invalidate all the cookies that exist on different devices. The user might have logged in from several different browsers, and the user has the option to use "Remember me" that lasts 30 days.
My understanding to solve this problem so far:
Use a securityStamp (a GUID) that I store in the database at the user level
Add this securityStamp in the Claims at login
When logout => change the securityStamp in the database
When http request arrives on a method of controller with [Authorize] attribute, check if the securityStamp match the one stored in the database. If not, redirect to login page.
My question is about point 4) where and how write this securityStamp check in the ASP.net CORE framework and redirect to login page ?
Here is my code at login time
string securityStamp = Guid.NewGuid().ToString();
saveSecurityStampInDB(securityStamp, user.Id);
var userClaims = new List<Claim>()
{
new Claim("id", user.Id.ToString()),
new Claim("securityStamp", securityStamp),
new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", "ASP.NET Identity", "http://www.w3.org/2001/XMLSchema#string")
};
var grantMyIdentity = new ClaimsIdentity(userClaims, "User Identity");
var userPrincipal = new ClaimsPrincipal(new[] { grantMyIdentity });
if (rememberMe.HasValue && rememberMe.Value)
{
await HttpContext.SignInAsync(userPrincipal, new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddMonths(1)
});
}
else
{
await HttpContext.SignInAsync(userPrincipal);
}
UPDATE:
I have my own user table, I don't use entityFramework and the whole built-in Identity management.
You can use the SecurityStamp Property and the SecurityStampValidatorOptions.ValidationInterval Property to make the logout user's cookie invalid.
1.Register ValidationInterval in ConfigureServices
services.Configure<SecurityStampValidatorOptions>(options =>
{
options.ValidationInterval = TimeSpan.FromSeconds(1);//set your time
});
2.Add userManager.UpdateSecurityStampAsync()in your Logout like below
public async Task<IActionResult> Logout()
{
var userid = userManager.GetUserId(User);
var user = await userManager.FindByIdAsync(userid);
await userManager.UpdateSecurityStampAsync(user);
await signInManager.SignOutAsync();
return RedirectToAction("Index", "Home");
}
Result:
I have an api with many controllers. One of these controllers is the authentication one. It use to get a JWT token and call others paths, from this controller or another.
My problem is, for any paths, I need to get the user id in the JWT token, and ask to the database to get informations about user, like check if the user exist or datas linked to him. So in each method, I have to call a specific method to retrieve informations.
In fact, the authentication layer is only used in order to check if the JWT Token is valid. I don't know if is it possible to add some logic in this layer. And how.
Can I implement any pattern in order to retrieve automatically user's information ?
I thought about singleton, but I'm not sure about the scope of the object. The goal is to stay in the request scope.
I thought to create an User Service, but I think it is not a good way because services are about treatment, not keep datas in these.
I thought to implement a custom middleware, but I'm not sure about the way to do it.
The objective is to implement all the user logic in the same place, and each service can call it in order to deal with it :
Request (with JWT Token) --> Controller --> Service --> Call the object with all user's stuff and logic and treatment
or
Request (with JWT token) --> middleware (get all user's informations) --> Controller --> Call the object created in middleware with all user's stuff
For information, there are my login method and the way I create the JWT token :
public async Task<ServiceResponse<UserLoginResponseDto>> Login(ServiceRequest<UserLoginRequestDto> request)
{
ServiceResponse<UserLoginResponseDto> response = new ServiceResponse<UserLoginResponseDto>();
User user = await _context.Users.FirstOrDefaultAsync(x => x.Username.ToLower().Equals(request.Data.Username.ToLower()));
if (user == null)
{
response.Success = false;
response.Message = "User not found";
}
else if (user.IsLocked)
{
response.Success = false;
response.Message = "User locked";
}
else if (!user.IsActivated)
{
response.Success = false;
response.Message = "User not activated";
}
else if (!VerifyPasswordHash(request.Data.Password, user.PasswordHash, user.PasswordSalt))
{
response.Success = false;
response.Message = "Wrong password";
}
else
{
response.Data = _mapper.Map<UserLoginResponseDto>(user);
response.Data.JWtToken = CreateToken(user);
}
return response;
}
private string CreateToken(User user)
{
List<Claim> claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Username),
};
SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config.GetSection("AppSettings:Token").Value));
SigningCredentials creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature);
SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.Now.AddDays(1),
SigningCredentials = creds
};
JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
SecurityToken token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
We have a requirement to authenticate users in IdentityServer4 against an external API. The scenario works like this:
User visits a Javascript client application and clicks the login button to redirect to IdentityServer login page (exact same client as provided in the docs here
User enters their username (email) and password
IdentityServer4 connects to an external API to verify credentials
User is redirected back to the JavaScript application
The above process works perfect when using the TestUsers provided in the QuickStarts. However, when an API is used, the login page resets and does not redirect the user back to the JavaScript client. The only change is the below code and a custom implementation of IProfileService.
Below is the custom code in the login action (showing only the relevant part):
var apiClient = _httpClientFactory.CreateClient("API");
var request = new HttpRequestMessage(HttpMethod.Post, "/api/auth");
var loginModel = new LoginModel
{
Email = model.Email,
Password = model.Password
};
var content = new StringContent(JsonConvert.SerializeObject(loginModel),
Encoding.UTF8, "application/json");
request.Content = content;
HttpResponseMessage result = await apiClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
var loginStatus = JsonConvert.DeserializeObject<ApiLoginStatus>(
await result.Content.ReadAsStringAsync());
if (loginStatus.LoginSuccess)
{
await _events.RaiseAsync(new UserLoginSuccessEvent(model.Email, model.Email, loginStatus.Name, clientId: context?.ClientId));
AuthenticationProperties props = null;
if (AccountOptions.AllowRememberLogin && model.RememberLogin)
{
props = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
};
};
var user = new IdentityServerUser(loginStatus.SubjectId)
{
DisplayName = loginStatus.Name
};
await HttpContext.SignInAsync(user, props);
if (context != null)
{
if (await _clientStore.IsPkceClientAsync(context.ClientId))
{
return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl });
}
return Redirect(model.ReturnUrl);
}
The code actually hits the return View() path, but for some reason it resets and the login page is shown again.
Code in Startup.cs:
var builder = services.AddIdentityServer()
.AddInMemoryIdentityResources(Config.Ids)
.AddInMemoryApiResources(Config.Apis)
.AddInMemoryClients(Config.Clients)
.AddProfileService<ProfileService>()
.AddDeveloperSigningCredential();
Code in ProfileService.cs:
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var profile = await GetUserProfile(context.Subject.GetSubjectId());
var claims = new List<Claim>
{
new Claim(ClaimTypes.Email, profile.Email),
new Claim(ClaimTypes.Name, profile.Name)
};
context.IssuedClaims.AddRange(claims);
}
public async Task IsActiveAsync(IsActiveContext context)
{
var profile = await GetUserProfile(context.Subject.GetSubjectId());
context.IsActive = (profile != null);
}
There are multiple sources online that show how to user a custom store for authentication, but they all seem to use ResourceOwnerPasswordValidator. If someone could point out what is missing here, it would help greatly. Thanks.
So the issue turned out to be very simple. We had missed removing the builder.AddTestUsers(TestUsers.Users) line when setting up IdentityServer in Startup.cs.
Looking at the code here, it turned out that this line was overriding our profile service with the test users profile service. Removing that line solved the problem.
How do I get the id_token for the implicit token to pass in the id_token hint for logout for implicit flow or is there another way? I have the end point /connect/endsession?
id_token_hint=
Not sure how I get the id_token from the implict flow all I get is a access_token and expiration. Is there a setting in IdSvr?
There's three components to this.
First ensure you're requesting an id_token from Identity Server when you're configuring the OIDC authentication in your Startup.cs (as mentioned by #leastprivilege above):
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44301/",
...
ResponseType = "id_token token", //(Here's where we request id_token!)
Secondly, using the OIDC notifications & after the security token is validated you add the id_token to your user's claims:
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = async n =>
{
var nid = new ClaimsIdentity(
n.AuthenticationTicket.Identity.AuthenticationType,
Constants.ClaimTypes.GivenName,
Constants.ClaimTypes.Role);
// get userinfo data
var userInfoClient = new UserInfoClient(
new Uri(n.Options.Authority + "/" + Constants.RoutePaths.Oidc.UserInfo),
n.ProtocolMessage.AccessToken);
var userInfo = await userInfoClient.GetAsync();
userInfo.Claims.ToList().ForEach(ui => nid.AddClaim(new Claim(ui.Item1, ui.Item2)));
// keep the id_token for logout (**This bit**)
nid.AddClaim(new Claim(Constants.TokenTypes.IdentityToken, n.ProtocolMessage.IdToken));
n.AuthenticationTicket = new AuthenticationTicket(
nid,
n.AuthenticationTicket.Properties);
},
Finally, on the redirect for signout (also a notification event) you add the id_token to the Protocol Message:
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst(Constants.TokenTypes.IdentityToken);
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
}
}
return Task.FromResult(0);
}
You'll also need to ensure you setup the PostLogoutRedirectUris on the client within Identity Server:
new Client
{
Enabled = true,
ClientName = "(MVC) Web App",
ClientId = "mvc",
Flow = Flows.Implicit,
PostLogoutRedirectUris = new List<string>
{
"https://localhost:44300/" //(** The client's Url**)
}
}
That will ensure you give the user an option to return to the authorised client when they log out :)
All of this is pretty much as per the MVC Sample at https://identityserver.github.io/Documentation/docsv2/overview/mvcGettingStarted.html
Bit more than you asked for but hopefully that helps anyone else who's trying to figure it out too :)
To get an id_token, you have to ask for it. Use response_type=id_token token
Have you tried this?
ASP.Net Identity Logout
It should create the id token hint automatically
This initial login succeeds:
public static MobileServiceClient MOBILE = new MobileServiceClient("https://myapp.azure-mobile.net/",myApplicationKey);
MobileServiceAuthenticationProvider GOOGLEPROVIDER = MobileServiceAuthenticationProvider.Google;
private async Task Connect() {
var USER = await MOBILE.LoginAsync(this, GOOGLEPROVIDER);
var CACHE = new Dictionary<string, string> { { "token", USER.MobileServiceAuthenticationToken } };
var ACCOUNT = new Account(USER.UserId, CACHE);
var STORE = AccountStore.Create(this);
STORE.Save(ACCOUNT, "Google");
}
but then this attempt to reuse the token to reconnect without a login page fails:
public async Task Reconnect() {
var STORE = AccountStore.Create(this);
var token = STORE.FindAccountsForService("Google").ToArray()[0].Properties["token"];
// token seems ok
var jsonToken = new JObject();
jsonToken.Add("access_token", token);
var USER = await MOBILE.LoginAsync(MobileServiceAuthenticationProvider.Google, jsonToken); // BOOM!
}
... with the following message: "The POST Google login request must contain both code and id_token in the body of the request."
What I am getting wrong here?
The token you use in the code, viz.
var CACHE = new Dictionary { { "token",USER.MobileServiceAuthenticationToken } };
The MobileServiceAuthenticationToken above is a token specific to MobileServices and cannot be used in the LoginAsync method (LoginAsync method requires a Google OAuth token.)
Please see this Get User Info from Google Api Using Azure Mobile Services for Xamarin Android