Cookie Authentication: How to access a returning user - asp.net-core

I have studied a couple examples of ASP.Net Core 2.0 Cookie Authentication.
However, I still don't see the code that checks for a returning user via a cookie.
Please note that I'm not a web developer. As a result, I realize that my question may appear foolish.
As I understand, the browser sends the cookie to the server per client request. However, I just don't see the server logic for this in the examples that I've studied. Hence, I only see logic for logging in the user the very first time based a username and password that's explicitly passed in.
Expectation:
I expected the server to explicitly check if a cookie exists when requesting the index page of the website. If a cookie does exist, I would expect to see some logic to address a user that's already logged in.
Giraffe example
Tutorial example
Here's my code:
let loginHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
Tasks.Task.Run(fun _ -> StackOverflow.CachedTags.Instance() |> ignore) |> ignore
task {
let! data = ctx.BindJsonAsync<LogInRequest>()
let email = data.Email.ToLower()
if authenticate email data.Password
then match login email with
| Some provider ->
let claims = [ Claim(ClaimTypes.Name, email) ]
let identity = ClaimsIdentity(claims, authScheme)
let user = ClaimsPrincipal(identity)
do! ctx.SignInAsync(authScheme, user)
return! json provider next ctx
| None -> return! (setStatusCode 400 >=> json "Invalid login") next ctx
else return! (setStatusCode 400 >=> json "Invalid login") next ctx
}

I'm looking at the giraffe example.
The statements services.AddAuthentication(authScheme) and services.AddCookie(cookieAuth) will add various services to the services collection, making them available for injection.
The statement app.UseAuthentication() adds services to the middleware pipeline. A middleware service is something that runs on every request.
The service that gets added by UseAuthentication is called AuthenticationMiddleware. As you can see this middleware relies on an injected IAuthenticationSchemeProvider (provided by your call to AddAuthentication) which in turn (skipping a few steps here) relies on a service called CookieAuthenticationhandler (provided by your call to AddCookie) which actually does the work of authenticating via cookie. When it is done it sets the logged in user onto the HttpContext which you can reference from your controllers if you need to.

Related

How does a Servant client handle received cookies?

I want to use a Servant client to first call a login endpoint to obtain a session cookie and then make a request against an endpoint that requires cookie authentication.
The API is (simlified)
import qualified Servant as SV
import qualified Servant.Auth.Server as AS
import qualified Servant.Client as SC
-- Authentication and X-CSRF cookies
type CookieHeader = ( SV.Headers '[SV.Header "Set-Cookie" AS.SetCookie
, SV.Header "Set-Cookie" AS.SetCookie]
SV.NoContent )
type LoginEndpoint = "login" :> SV.ReqBody '[SV.JSON] Login :> SV.Verb 'SV.POST 204 '[SV.JSON] CookieHeader
type ProtectedEndpoint = "protected" :> SV.Get '[SV.JSON]
-- The overall API
type Api = LoginEndpoint :<|> (AS.Auth '[AS.Cookie, AS.JWT] User :> ProtectedEndpoint)
apiProxy :: Proxy Api
apiProxy = Proxy
I define the client as follows:
loginClient :: Api.Login -> SC.ClientM Api.CookieHeader
protectedClient :: AC.Token -> SC.ClientM Text :<|> SC.ClientM SV.NoContent
loginClient :<|> protectedClient = SC.client Api.apiProxy
How does the client handle the authentication cookie? I can think of two ways. When executing a request in the ClientM monad, like
do
result <- SC.runClientM (loginClient (Login "user" "password")) clientEnv
[..]
where Login is the login request body and clientEnv of type Servant.Client.ClientEnv, the cookie could part of the result, it could be updated in the cookieJar TVar inside clientEnv, or both. I would assume the TVar to be updated, so that a subsequent request with the same clientEnv would send the received cookies along. However, my attempt to read the TVar and inspect its contents using Network.HTTP.Client.destroyCookieJar revealed an empty array. Is this intended? I couldn't find anything in the documentation.
Thus, to make an authenticated call, I would need to extract the cookie from the header in the result (how?), update the TVar, create a new clientEnv that references this TVar, and make the authenticated call using this new environment. Is this indeed the suggested procedure? I'm asking because I suppose the use case is so standard that there should be a more streamlined solution. Is there? Am I missing something?
After some experimentation, I figured out that the Servant client indeed does maintain cookies in the cookieJar that is part of the clientEnv. To be more precise, clientEnv contains the field cookieJar, which is of type Maybe (TVar CookieJar). It is the TVar the client updates according to the Set-Cookie instructions of subsequent requests. It is up to the developer to create and initialize that TVar before making the first request; otherwise, the Servant client will discard cookies between requests.
In addition, it is possible to retrieve cookies in the same way as the request body. To this end, the cookies to be retrieved must be defined as part of the API type, like in the example of my original question:
type LoginEndpoint = "login" :> SV.ReqBody '[SV.JSON] Login :> SV.Verb 'SV.POST 204 '[SV.JSON] CookieHeader
Disassembling the returned was a little tricky at first, because I needed to figure out the final type that results from Servant's type-level machinery. Ultimately, I did the following:
SV.Headers resp h <- tryRequest clientEnv (loginClient (Api.Login "user" "pwd"))
let headers = SV.getHeaders h
where tryRequest is a helper to execute runClientM and extract the Right part. The pattern match resp contains the return value (here NoContent), while h is is an HList of the different headers. It can be converted into a regular list of Network.HTTP.Types.Header using Servant's getHeaders function.
It would then be possible to change or generate new headers and submit them with a new request by adding a new header to the cookieJar TVar (see the cookie-manipulating functions in Network.HTTP.Client).

Adding and accessing claims in asp net core 3.0 using built in Identity server

I'm currently failing at wrapping my head around claims. I have a ASP.Net Core 3 project with the angular template and users stored in app.
I want to add claims to my users, reading up on I thought it would be easy, just add something along the lines of
await _UserManager.AddClaimAsync(user, new Claim(AccountStatic.ClaimTypes._Claim_Id, user.Id));
When you create the user, and then get it back using the below line once they are logged in again:
User.FindFirst(AccountStatic.ClaimTypes._Claim_Id)?.Value;
This does however not work. I can see the claims being written to AspNetUserClaims table in my database but it's not there in the users claims when they log in. There are a few other claims there, but not the ones I have added.
Do I need to define somewhere which of the users claims get included when they log in?
Edit.
I found a post stating that I need to add claims using a DI AddClaimsPrincipalFactory. So I added this class.
public class UserClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser>
{
public UserClaimsPrincipalFactory(UserManager<ApplicationUser> userManager,IOptions<IdentityOptions> optionsAccessor): base(userManager, optionsAccessor)
{}
//https://levelup.gitconnected.com/add-extra-user-claims-in-asp-net-core-web-applications-1f28c98c9ec6
protected override async Task<ClaimsIdentity> GenerateClaimsAsync(ApplicationUser user)
{
var identity = await base.GenerateClaimsAsync(user);
identity.AddClaim(new Claim(AccountStatic.ClaimTypes.Claim_Id, user.Id ?? "[no id]"));
return identity;
}
}
And if I step through the code I can see the claims being added here. But in the Controller my custom claims are not present.
internal string GetUserId()
{
if (User.Identity.IsAuthenticated == false)
return null;
return User.FindFirst(AccountStatic.ClaimTypes.Claim_Id)?.Value;
}
Update. Ok I find this very strange. I have been trying to do what others claim work but for me nothing gets me the users name or id. inspecting the User I get the following. Nothing here contains any reference to the logged in user.
Update 2:
Just noticed that there is actually an Id in there: {http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier: ed107a11-6c62-496b-901e-ed9e6497662a} Seems to be the users id from the database. Not sure how to access it yet though.
These return null.
User.FindFirst(JwtRegisteredClaimNames.NameId)?.Value;
User.FindFirst("nameidentifier")?.Value;
User.FindFirst("NameIdentifier")?.Value;
Another update
I'm using a UserClaimsPrincipalFactory and breakingpointing it and looking at the Claims I can see that all of the ones I want are there. But again, these are not available in my API controllers as seen in the first picture.
I finally understood the problem, in large parts thanks to Ruard van Elburgs comments, and the answer he made in the linked question IdentityServer4 Role Based Authorization.
The problem is that the claims are not added to the access token.
There are two tokens, the access token and the identity token.
- Ruard van Elburg
They key to understanding what was going on was finding out that there are two tokens, and that they contain different claims and have different purposes.
You can force claims from one token to also be included in the other if you deem it necessary.
The solution to my problem was to add this in Startup.ConfigureServices
services
.AddIdentityServer(options => {})
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options =>
{
foreach (var c in options.ApiResources)
{
// the string name of the token I want to include
c.UserClaims.Add(AccountStatic.ClaimTypes.Claim_Id);
}
});
I still have not figured out how to get the Identity token, but as I'm now including the user Id in the access token my problems are solved for the moment.

intermittent error from rally 'Not authorized to perform action: Invalid key' for POST request in chrome extension

I developed a chrome extension using Rally's WSAPI v2.0, and it basically does the following things:
get user and project, and store them
get current iteration everytime
send a post request to create a workitem
For the THIRD step, I sometimes get error ["Not authorized to perform action: Invalid key"] since end of last month.
[updated]Error can be reproduced everytime if I log in Rally website via SSO before using the extension to send requests via apikey.
What's the best practice to send subsequent requests via apikey in my extension since I can't control end users' habits?
I did see some similar posts but none of them is helpful... and in case it helps:
I'm adding ZSESSIONID:apikey in my request header, instead of user /
password to authenticate, so I believe no security token is needed
(https://comm.support.ca.com/kb/api-key-and-oauth-client-faq/kb000011568)
url starts with https://rally1.rallydev.com/slm/webservice/v2.0/
issue is fixed after clearing cookies for
https://rally1.rallydev.com/, but somehow it appears again some time
later
I checked the cookie when the issue was reproduced, and found one with name of ZSESSIONID and its value became something else rather than the apikey. Not sure if that matters though...
code for request:
function initXHR(method, url, apikey, cbFunc) {
let httpRequest = new XMLHttpRequest();
...
httpRequest.open(method, url);
httpRequest.setRequestHeader('Content-Type', ' application\/json');
httpRequest.setRequestHeader('Accept', ' application\/json');
httpRequest.setRequestHeader('ZSESSIONID', apikey);
httpRequest.onreadystatechange = function() {
...
};
return httpRequest;
}
...
usReq = initXHR ('POST', baseURL+'hierarchicalrequirement/create', apikey, function(){...});
Anyone has any idea / suggestion? Thanks a million!
I've seen this error when the API key had both read-only and full-access grants configured. I would start by making sure your key only has the full-access grant.

Google OpenIDConnect: Why am I not getting an 'openid_id' value along with 'sub'?

I've read all the documentation I can find on migrating from Google OpenID 2 to OAuth 2/OpenIDConnect, and am currently using a nice class from phpclasses.org . This class seems to work quite well with both Google and Facebook (haven't yet tried other providers), but I'm having a problem with just one aspect of Google's migration path that is quite critical to me: obtaining the google user's old OpenID identifier in addition to the new OpenIDConnect 'sub' value for that user. I've got users registered in my database only through their old OpenID identifiers.
According to Step 3 in Google's Migration Guide it looks like all I should need to do is add a parameter "openid.realm=http://www.example.com" to the authentication request sent to https://accounts.google.com/o/oauth2/auth.
I looked up in my old code what the realm was that I used for its OpenID registration process (it was 'http://' . $_SERVER['HTTP_HOST'];), and then I made sure that the redirect urls in my application were compatible with that realm.
I added that value (url-encoded) as the value of an openid.realm parameter passed on the authentication request made within the class. But when the class exchanged the token for an access token, it got back the correct email, name, sub, etc, but there was no openid_id parameter present. BTW, my scope parameter is 'openid email profile'
Does anyone have a suggestion for what else I should try, or what I can do to determine what the problem is? Does anyone have successful experience getting the openid_id parameter value in php code? I'd really rather not go the client-side route with their "Sign-in with Google" button, and according to the docs that really shouldn't be necessary (plus there's no particular reason to believe it would solve my problem if I did it).
Just discovered it's in the id_token returned along with the access_token when you exchange the authorization_code for the access_token.
In the Migration Document, Step 3 first two paragraphs:
When you send an OpenID Connect authentication request URI to Google
as described in Step 1, you include an openid.realm parameter. The
response that is sent to your redirect_uri includes an authorization
code that your application can use to retrieve an access token and an
ID token. (You can also retrieve an ID token directly from the OpenID
Connect authentication request by adding id_token to the response_type
parameter, potentially saving a back-end call to the token endpoint.)
The response from that token request includes the usual fields
(access_token, etc.), plus an openid_id field and the standard OpenID
Connect sub field. The fields you need in this context are openid_id
and sub:
This is confusing and misleading/wrong. What token request? The authentication request returns an authorization code that you can exchange for an access_token and an id_token. The parenthetical remark about adding id_token to the response_type doesn't help much, as the various ways I tried to do that resulted in an error. But in any event, the
"usual fields (access_token, etc.), plus an openid_id field..."
is wrong. The access_token never appears in the same list at the openid_id field. The access_token appears in a list with the id_token, and the openid_id field is encoded within the id_token!
For testing purposes, you can decode an id_token using https://www.googleapis.com/oauth2/v1/tokeninfo?id_token=<string>
In this documentation I couldn't find a useful description for how to decode an id_token, only caveats about their being sensitive, and how to validate them (though validation is not needed if obtained directly from a google endpoint as is the case here). I downloaded google's php client, and extracted code from it (src/Google/Auth/OAuth2.php and src/Google/Utils.php). And from that it's easy enough to figure out how to decode the id_token string: explode on ., base64_decode element 1, and json_decode that.
Update 2015-05-21: In reply to #Arthur's "answer", which would have been more appropriate as a comment on this answer. I would have commented on that answer myself, but comments aren't allowed to be very long and don't allow image uploads, plus I thought this extra info improves my answer...
Below is a screenshot from netbeans/xdebug, showing the array elements I get when decoding the id_token I get. Interesting that the intersection of the fields listed here with the fields listed by #Arthur is the null set. So I suspect that whatever #Arthur is decoding, it is not an id_token of the kind described here. I'm not familiar enough with this stuff even to guess what it is that's being decoded in that answer.
I'm afraid I don't have the time to dig through the library I use to extract the exact code path that produces the id_token I decoded to get this array using the simple algorithm I described. But I can tell you that the library I use is this: http://www.phpclasses.org/package/7700-PHP-Authorize-and-access-APIs-using-OAuth.html
Using it just as documented does not give you the id_token you need for this for two reasons:
The pre-configured server for Google with Oauth 2 doesn't handle the openid.realm parameter. To handle that, I added the following server definition to the oauth_configuration.json file:
"Google-OpenIdConnect":
{
"oauth_version": "2.0",
"dialog_url": "https://accounts.google.com/o/oauth2/auth?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope={SCOPE}&state={STATE}&openid.realm={REALM}",
"offline_dialog_url": "https://accounts.google.com/o/oauth2/auth?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope={SCOPE}&state={STATE}&access_type=offline&approval_prompt=force",
"access_token_url": "https://accounts.google.com/o/oauth2/token"
},
Just after the call to Initialize(), you need to add
$client->store_access_token_response = true;
Without that, the actual access_token response is not accessible (at least not the way I'm using the class). With those two changes in place, my exact code to get the openid_id using this class is as follows:
protected function jwt_decode($jwt) {
$segments = explode(".", $jwt);
if (count($segments) != 3) {
throw new Exception("Wrong number of segments in token: $jwt");
}
// Parse envelope.
$envelope = json_decode($this->urlSafeB64Decode($segments[0]), true);
if (!$envelope) {
throw new Exception("Can't parse token envelope: " . $segments[0]);
}
// Parse token
$json_body = $this->urlSafeB64Decode($segments[1]);
$payload = json_decode($json_body, true);
return $payload;
}
protected function getOpenid_id() {
require_once 'Phpclasses/Http/Class.php';
require_once 'Phpclasses/OauthClient/Class.php';
require 'Phpclasses/Google/private/keys.php';
$client = new oauth_client_class;
$client->configuration_file = $phpclasses_oauth_dir . '/oauth_configuration.json';
$client->server = 'Google-OpenIdConnect';
$client->redirect_uri = 'http://' . $_SERVER['HTTP_HOST'] . strtok($_SERVER['REQUEST_URI'], '?');
$client->client_id = $GOOGLE_APPID;
$client->client_secret = $GOOGLE_APPSECRET;
$client->scope = 'openid email';
$client->realm = $this->getRequest()->getScheme() . '://' . $this->getRequest()->getHttpHost();
$me = null;
if (($success = $client->Initialize())) {
// set *after* the call to Initialize
$client->store_access_token_response = true;
if (($success = $client->Process())) {
if (strlen($client->authorization_error)) {
$client->error = $client->authorization_error;
$success = false;
}
elseif (strlen($client->access_token)) {
$success = $client->CallAPI('https://www.googleapis.com/oauth2/v1/userinfo', 'GET', array(), array('FailOnAccessError' => true), $user);
$me = (array) $user;
if (!array_key_exists('id_token', $client->access_token_response)) {
throw new Exception('No id_token in \$client->access_token_response');
}
$openid_id = $this->jwt_decode($client->access_token_response['id_token']);
$me['openid_id'] = $openid_id;
}
}
$success = $client->Finalize($success);
}
if ($client->exit)
exit;
$client->ResetAccessToken();
if ($success) {
return $me;
}
// Code to handle failure...
}
Despite sootsnoot's (own) answer I still can't find the openid_id field anywhere. When decoding the id_token there are only "issuer", "issued_to", "audience", "user_id" , "expires_in" , "issued_at", "email" and "nonce" fields.
No "openid_id" field in sight..
Any ideas?
In response to sootsnoot's response :) And I apologize for not having enough reputation to comment, otherwise would have done so.
Am using an OpenID Connect library that takes endpoints from auto-config: https://accounts.google.com/.well-known/openid-configuration
So assume the endpoints are not the problem. Indeed it seems I was checking the wrong id_token. However, even when checking the correct one I still don't see the "openid_id" field. I now see everything you have, except that I have a "nonce" field instead of the "openid_id" field:
stdClass::__set_state(array( 'iss' => 'https://accounts.google.com', 'sub' => ****, 'azp' => ****, 'email' => ****, 'nonce' => ****, 'at_hash' => ****, 'email_verified' => true, 'aud' => ****, 'iat' => ****, 'exp' => 1432300788, ))
Must be doing something wrong, but what...
Final update:
Found the issue: was passing realm parameter as openid_realm=... instead of openid.realm=...
Oh do I feel stupid... :)

How do you use Snap's authentication mechanisms during a single POST request?

I'm working on a Haskell Snap-based web app, and I want to expose an API endpoint that will be invoked by a remote service without establishing an authenticated session a-priori; however, I do want that request to be authenticated, so the credentials should be provided at the time of the request.
You could imagine the request containing four fields:
username
password
payload id
payload file
The payload id and file might be irrelevant for this question, but I include them because I (a) need to support file uploads in this request (which, as I understand it, restricts the encoding used to send fields) and (b) need to retrieve at least one non-file field. The combination of those things posed some difficulty when I set this up without authentication, so perhaps it is relevant.
In Snap parlance, let's call this handler uploadHandler.
As indicated above, I have this working fine without authentication, with a setup like this:
uploadHandler :: Handler App App ()
uploadHandler = do
-- collect files / form fields and process as needed.
-- and using the routes:
routes :: [(ByteString, Handler App App ())]
routes = [ ("/login", with auth handleLoginSubmit)
, ("/logout", with auth handleLogout)
, ("/new_user", with auth handleNewUser)
-- handle the upload:
, ("/upload", handleUpload)
]
The naive solution is to simply add 'with auth' and change the type of handleUpload:
uploadHandler :: Handler App (AuthManager App) ()
uploadHandler = do
-- collect files / form fields and process as needed.
-- and using the routes:
routes :: [(ByteString, Handler App App ())]
routes = [ ("/login", with auth handleLoginSubmit)
, ("/logout", with auth handleLogout)
, ("/new_user", with auth handleNewUser)
-- handle the upload, with auth:
, ("/upload", with auth handleUpload)
]
However, this seems to require two requests: (i) authenticate and establish a session, (ii) send the POST request containing the actual payload.
I found a way to do this in one request, but it seems like there should be a more elegant means. Here's the example restricted POST handler I've hacked together:
restrictedPOST :: Handler App (AuthManager App) ()
restrictedPOST = do
mName <- getPostParam "username"
mPass <- getPostParam "password"
let uName = C8.unpack $ fromMaybe "" mName
pass = ClearText $ fromMaybe "" mPass
authResult <- loginByUsername (T.pack uName) pass False
case authResult of
Left authFail -> writeText "Could not log in"
Right user -> writeText (T.append "Hello " (userLogin user))
Is there something like 'with auth' that I can use instead of turning this example (restrictedPOST) into a combinator? I realize it may need to know which fields to get credentials out of, but I also know very little about web services (maybe there is another means? Maybe this is a total non-issue, and I just don't know how to check auth for POST requests. I'm open to any suggestions!)
I don't think you understand what with auth is doing. It has nothing to do with whether authentication is required. All it does is convert a Handler b (AuthManager b) into a Handler b v. No permissions checks are performed. Your restrictedPOST function has the right idea.