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

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.

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).

python requests login with redirect

I'd like to automate my log in to my bank to automatically fetch my transactions to stay up-to-date with spendings and earnings, but I am stuck.
The bank's login webpage is: https://login.bancochile.cl/bancochile-web/persona/login/index.html#/login
I am using python's request module with sessions:
urlLoginPage = 'https://login.bancochile.cl/bancochile-web/persona/login/index.html'
urlLoginSubmit = 'https://login.bancochile.cl/oam/server/auth_cred_submit'
username = '11.111.111-1' # this the format of a Chilean National ID ("RUT")
usernameFormatted = '111111111' # same id but formatted
pw = "password"
payload = [
("username2", usernameFormatted),
("username2", username),
("userpassword", pw),
("request_id", ''),
("ctx", "persona"),
("username", usernameFormatted),
("password", pw),
]
with requests.Session() as session:
login = session.get(urlLoginPage)
postLogin = session.post(
urlLoginSubmit,
data=payload,
allow_redirects=False,
)
redirectUrl = postLogin.headers["Location"]
First I find that the form data has duplicated keys, so I am using the payload as a list of tuples. From Chrome's inspect I find the form data to be like this:
username2=111111111&username2=11.111.111-1&userpassword=password&request_id=&ctx=persona&username=111111111&password=password
I've checked the page's source code to look for the use of a csrf token, but couldn't find any hint of it.
What happens is that the site does a redirect upon submitting the login data. I set allow_redirects=False to catch the redirect url of the post under the Location-header. However, here is the problem. Using the web-browser I know that the redirect url should be https://portalpersonas.bancochile.cl/mibancochile/rest/persona/perfilamiento/home, but I always end up on an error page when using the above method (https://login.bancochile.cl/bancochile-web/contingencia/error404.html). (I am using my own, correct login credentials to try this)
If I submit the payload in a wrong format (e.g. by dropping a key) I am redirected to the same error-page. This tells me that probably something with the payload is incorrect, but I don't know how to find out what may be wrong.
I am kind of stuck and don't know how I can figure out where/how to look for errors and possible solutions. Any suggestions on how to debug this and continue or ideas for other approaches would be very welcome!
Thanks!

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.

Cookie Authentication: How to access a returning user

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.

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... :)