Session empty after redirect - ruby-on-rails

I've a React JS app, which makes this request to my back-end API. i.e
window.location = "https://my-server.com" + "/gmail/add_account";
cannot set HTTP headers for window.location see this
this server endpoint redirects to Google OAuth page, which returns a response to my redirect_uri.
def add_account
# no auth headers sent here, because front-end has used window.location
gmail_service = GmailService.new
session[:uid] = params["uid"]
redirect_to gmail_service.generate_authorization_url()
end
def oauth_postback
# session object is {} here
# Since there are no authorization headers, I cannot identify my app's user
# How can I identify my app's user here?
end
The problem I'm facing is that when the OAuth flow sends the response to my redirect_uri it does not return include any authorization header, due to which I'm unable to identify which user of my app has launched this OAuth flow.
I've tried setting up a session variable in the /gmail/add_account endpoint, which works fine. After this endpoint redirects to the OAuth screen, and the Oauth flow sends a response to my Oauth redirect_uri, there my session object is {}.
How can I implement this flow such that I know which user has launched this OAuth flow?

You have basically two options:
the state parameter
The state parameter is part of the OAuth2 spec (and is supported by Google). It's a random string of characters that you add to the authorization URL (as a query parameter), and will be included when the user is redirected back to your site (as a query parameter). It's used for CSRF protection, and can also be used to identify a user. Be sure that if you use it, it's a one-time value (e.g. a random value that you store in your db, not the user's ID).
sessions with cookies
If the user has previously logged in, you should be able to identify them by their session cookie. It sounds like this is the approach you're currently taking, but the session is getting reset.
It's difficult to debug this without knowing more about your stack/code, but a good first step would be just trying to load your callback URL without the redirection to Google to see the session object is still empty. If so, that would indicate an issue with how you've implemented sessions generally and not something specific to this flow.
As a note, based on the code you've shared, I'm not sure how params["uid"] is getting set if you're doing a redirect without any query parameters or path parameters.
Finally, you may consider using a managed OAuth service for something like this, like Xkit, where I work. If you have a logged in user, you can use Xkit to connect to the user's Gmail account with one line of code, and retrieve their (always refreshed) access tokens anywhere else in your stack (backend, frontend, cloud functions) with one API call.

Related

OAuth 2.0: why is the access token or temporary code placed in the in the URL fragment (after the #) instead of in the query string?

I am learning OAuth 2.0.
In both code flow and implicit flow (response_type = code or token). The temporary code or access_token is placed in the URL fragment (after the #) instead of in the query string.
According to this doc: https://developer.okta.com/blog/2018/05/24/what-is-the-oauth2-implicit-grant-type:
If the user approves the request, the authorization server will redirect the browser back
to the redirect_uri specified by the application, adding a token and state to the fragment
part of the URL.
For example, the user will be redirected back to a URL such as:
https://example-app.com/redirect
#access_token=g0ZGZmNj4mOWIjNTk2Pw1Tk4ZTYyZGI3
&token_type=Bearer
&expires_in=600
&state=xcoVv98y2kd44vuqwye3kcq
Note the two major differences between this and the Authorization Code flow: the access token is returned
instead of the temporary code, and both values are returned in the URL fragment (after the #) instead
of in the query string. By doing this, the server ensures that the app will be able to access the value
from the URL, but the browser won’t send the access token in the HTTP request back to the server.
What exactly does it mean by
the server ensures that the app will be able to access the value from the URL, but the browser won’t send the access token in the HTTP request back to the server.
?
Of course the code/ access_token value is accessible from the URL.
The Auth server builds up the url and put it in location header of the HTTP response, which is sent back to the user's web browser. The web browser then take values from the response, and send new http requests to the application instead of the Auth server. So, of course the user's web browser is not sending HTTP request back to the server.
It has nothing to do with where the access token is placed in the response (from Auth server back to the user's web browser). The web browser simply starts talking to the application again instead of the Auth server.
This explanation just does not make much sense to me.
The code flow returns an authorization code to the browser in the query string. You then make a POST request to swap the code for tokens.
https://www.example.com?code=xxx&state=yyy
Implicit flow is now deprecated since it can reveal tokens in the browser history or server logs. It dates back to when browsers did not have CORS capabilities to make cross orign POST requests to the Authorization Server.
Data in client side hash fragments does not get sent to servers and the implicit flow used this as a partial mitigation for sensitive data. Eg the zzzz below does not get sent to the web server if you type this in a browser.
https://www.example.com#zzzz
If you are new to OAuth and OpenID Connect, start with code flow + PKCE, and understand these messages.
SWAPPING THE CODE FOR TOKENS
This is often done via a back end component that completes the flow, so that a client secret can be attached (a browser cannot keep secrets). This back end component can then do either of these:
Return access tokens to the browser
Issue secure cookies to the browser
Once done, the front end has a credential with which it can call the main back end (eg APIs), so that the back end knows the user identity.
CODE EXAMPLE
In 2021 the cookie option is considered more secure, but it also requires a more complex flow. All of the complexity involved originates from the browser being a hostile place to execute code - there are lots of security threats there. Here is some example code that uses cookies:
OAuth calls from an SPA
API calls from an SPA

Attempting to connect to a IdentityServer4 login page fails

I have an existing website that I want to do a proof of concept with OAuth2 / OIDC. To this end I've configured a locally running IdentityServer4 MVC app as my demo OIDC server following the IdentityServer4 quick setup guidelines. This works fine and navigating to:
http://localhost:5000/.well-known/openid-configuration
Lets me see the discovery document.
I have created a fake login page on this OIDC app which consists of just a login button with no user credentials required.
There's no actual user database and I'm just hard coding some user details to return when the 'authentication' occurs.
In my pre-existing site I've added the OWIN middle wear and am configuring OIDC using the OpenIdConnectAuthenticationOptions. The clientId, scopes, secret etc all match as required and the authority is set to point to my locally running demo OIDC app (http://localhost:5000). The redirect url is set to return to my pre-existing site once authentication is complete.
This all appears to be fine but here's what I want to achieve and can't get working. On my pre-existing site when I navigate to any page that requires authentication I want the user to be redirected to the login page I created on OIDC app. They click the login button (no user details required) and are authenticated and redirected back to the original page.
Currently when I navigate to a protected page I am successfully redirected to the OIDC app but I am redirected to an error page and I don't know why. The error page gives me no detail, it's actually hard coded in the app.
When I look at the discovery document I see that the setting for the 'authorization_endpoint' is set to:
http://localhost:5000/connect/authorize
So I thought maybe I needed to either change that to point to Home/Login which is where I've created the dummy login form, or else I needed to actually create that connect/authorize endpoint and put my form there. Creating the end point makes no difference, it never gets hit and instead I just get the error page on my OIDC app. Changing it to home/login also appears to be ignored.
I am away from my main PC at the moment hence the lack of code snippets but essentially the set up is as per the IdentityServer4 quick setup guide and the OIDC app does appear to be working.
The issue is getting my pre-existing site to properly redirect to the login page.
I've been stuck on this for quite a while now and would like to even get to the stage of seeing the dummy login page. Any pointers are appreciated and again apologies for the lack of sample code.
UPDATE
I've got the login form appearing by setting the openidconfiguration like so:
Configuration = new OpenIdConnectConfiguration()
{
AuthorizationEndPoint = "http://localhost:5000/home/login"
}
However, this isn't logging me in when I click login. On that login action I'm doing this:
await HttpContext.SignInAsync("subjectId","username", authenticationProps);
And then redirecting back to my existing site. However it's not authenticating me and the redirect ends up being redirected back again to the login page.
UPDATE 2
I think the redirect URI should possibly be doing something more. Currently I do the following:
Try and access a restricted page -> Redirected to OIDC server -> Click Login (this sets the subject and user successfully) -> Am redirected to redirect URI which immediately bounces me back to the OIDC server.
So maybe the redirect URI is supposed to confirm login or otherwise do something?
So in the open id connect protocol, the authorize endpoint is used to validate the client information passed as query parameters (client_id, scopes, redirect_uri, etc). In your authentication server, none of that is being checked if all the endpoint does is return a form. Then again the validation can be tedious so keeping the authorize endpoint separate from the endpoint for logging in might be worth a thought.
The developers of Identity Server thought the same thing which is why they set up the endpoint (and endpoint validation) for you as part of the middleware. The validation uses the components that were injected (primarily the client_store, and your defined scopes) to be used by Identity Server.
After the framework validates your authorize request using your client store implementation, it will redirect the user to whichever login page you specify. The login page can be specified by changing it with the a delegate that can be passed in as the second parameter of 'AddIdentityServer' (that takes in something of type IdentityServerOptions that we'll refer to as just 'options'). In the delegate you can specify the login url of the page by changing the value of 'options.UserInteraction.LoginUrl' to the url of the login page.
After the user logs in and you call the signInAsync method on the HttpContext, you're actually supposed to redirect back to a query parameter passed to the login page referred to as the 'return_url' (which is basically the initial authorize endpoint request). This authorize endpoint further validates the cookie and will send the user back to the 'redirect_uri' (if consent on the client is set to false) with a code (if using the authorization code flow) or the id_token and optionally the access token (if using the implicit flow).
Assuming the implicit flow for simplicity, the tokens can be found in the request to the 'redirect_uri' and from there it's all up to you. Commonly the client will issue some kind of cookie (which can potentially contain the id or access token) to mark the successful authentication by the identity provider.

Passport & JWT & Google/Facebook Strategy - How do I combine JWT and Google/Facebook Strategy?

This question is for anyone who is familiar with
Node.js
Express
Passport
JWT Authentication with passport (JSON Web Tokens)
Facebook OAuth2.0 OR Google OAuth2.0
I have been doing some online courses and understand how to do the two following things:
Authentication using Passport Local Strategy + JWT Tokens
Authentication using Passport Google/Facebook Strategy + Cookie/sessions.
I am trying to combine the content from these two courses basically. I want to use Google Strategy + JWT Authentication. I want to use JWT instead of cookies because my app is going to be a web/mobile/tablet app, and I need to be accessing the api from different domains.
There are two issues I am having with this:
To kick off the Google/facebook OAuth pipelines, you need to call either '/auth/facebook' or '/auth/google'. Both Oauth flows work basically the same so when I say '/auth/google' from now on, I am referring to either. Now the issue I'm having is: On the client, do I call the '/auth/google' route with a href button link or an axios/ajax call? If I use the href or axios/ajax approach I am still getting problems with both solutions.
The href approach problem:
When I assign an <a> tag with a href to '/auth/google' the authentication works perfectly fine. The user gets pushed through the Google Auth flow, they log in and the '/auth/google/callback' route gets called. The problem I have now is how do I correctly send the JWT token back to the client from '/auth/google/callback'?
After a lot of googling I have seen that people have simply passed the the JWT back to the client from the oauth callback in the redirect query param. For example:
res.redirect(301, `/dashboard?token=${tokenForUser(req.user)}`);
The issue I have with this is that now the the ability to authenticate is saved in my browser history! I could log out (destroying the token saved in localStorage), and then simply look at my browser url history, go back to the url that contains the token in the query param, and I would automatically log in again without having to go through the Google Strategy! This is a huge security flaw and is obviously the incorrect way to approach it.
The axios/ajax approach problem:
Now before I explain the problem with this issue, I know for sure that If I get this working, it will solve all issues I was having with the previous href problem. If I manage to call '/google/auth' from an axios.get() call and receive the JWT in the response body, I will not be sending the token as url param, and it will not get saved in the browser history! Perfect right? well there is still some problems with this approach :(
When try to call axios.get('/auth/google') I get the following error:
How I've tried to solve the problem:
I installed cors to my npm server, and added app.use(cors()); to my index.js.
I took a stab and added "http://localhost:3000" to the "Authorised JavaScript origins" in Google developer console.
Neither of these solutions solved the issue, so now I really feel stuck. I want to use the axios/ajax approach, but I'm not sure how to get past this cors error.
Sorry for such a long message, but I really felt I had to give you all the information in order for you to properly help me.
Thanks again, looking forward to hear from you!
I solved this in this way:
On Front-End (can be mobile app) I made login request to Google (or Facebook) and after the user selected his account and logged in I got back response that contained google auth token and basic user info.
Then I sent that google auth token to backend where my API sent one more request to the Google API to confirm that token. (See step 5)
After successful request comes you get basic user info and e-mail. At this point, you can assume that user login via Google is good since google check returned that it's okay.
Then you just signup or login user with that email and create that JWT token.
Return token to your client and just use it for future requests.
I hope it helps. I implemented this multiple times and it showed like a good solution.
Though there is good answer, I wanted to add more information with example.
Passport's google/facebook strategy is session based, it stores user info in cookie which is not advisable. So we need to disable it first
To disable session we need modify our redirect router. For example if we have redirect path /google/redirect like following, we need to pass { session: false } object as parameter.
router.get('/google/redirect', passport.authenticate('google', { session: false }), (req, res)=> {
console.log(":::::::::: user in the redirect", req.user);
//GENERATE JWT TOKEN USING USER
res.send(TOKEN);
})
So where does this user come from? This user comes from passport's callback function. In the previous snippet we have added passport.authenticate(....) This middlewire initiates passport's google-strategy's callback which deals with the user. For example
passport.use(
new GoogleStrategy({
callbackURL: '/google/redirect',
clientID: YOUR_GOOGLE_CLIENT_ID
clientSecret: YOUR_GOOGLE_SECRET_KEY
},
(accessToken, refreshToken, profile, done)=>{
console.log('passport callback function fired');
// FETCH USER FROM DB, IF DOESN'T EXIST CREATE ONE
done(null, user);
})
)
That's it. We have successfully combined JWT and Google/Facebook Strategy.
The solution I found was to do the OAuth flow in a pop-up (window.open), that makes use of a pre-defined callback to pass the token to the front-end upon successful authentication.
Below are the relevant code samples, taken from this tutorial:
https://www.sitepoint.com/spa-social-login-google-facebook/
Here is the pre-defined callback and initial open method, called from your front-end:
window.authenticateCallback = function(token) {
accessToken = token;
};
window.open('/api/authentication/' + provider + '/start');
And here is what your OAuth Callback URL should return, upon successful authentication (which is the last step/page inside your pop-up):
<!-- src/public/authenticated.html -->
<!DOCTYPE html>
<html>
<head>
<title>Authenticated</title>
</head>
<body>
Authenticated successfully.
<script type="text/javascript">
window.opener.authenticateCallback('{{token}}');
window.close();
</script>
</body>
</html>
Your token would now be available to your front-end's pre-defined callback function, where you could easily save it in localStorage.
I suppose though, you could do the OAuth flow in the same window then (sans pop-up) and return an HTML page (similar to the above) that just saves the token and redirects the user to a dashboard immediately.
If your front-end domain was different from your api/auth server, however, you would probably need to redirect from your api/auth server to your front-end with a single-use, time-sensitive token (generated by your api/auth server), that your front-end could then use to call and receive (with axios) your actual token. This way you wouldn't have that browser history security problem.

How to use Omniauth Asana with Rails API only app

I have a Rails 5 API only app and an Angular JS Frontend app and would like to integrate with Asana API. I'm using the ruby-asana, omniauth and omniauth-asana gems.
I start the request using Asana's JS library like so:
var client = Asana.Client.create({
clientId: 172706773623703,
clientSecret: '<client_secret>',
redirectUri: '<redirect_url>'
});
client.useOauth({
flowType: Asana.auth.PopFlow
});
And the above does redirect me to Asana where I can login. On the redirectUri I'm giving a backend route (Rails 5 API only) which should handle the remaining on the authentication (using the JS only I get only a temporary token that cannot be self renewed meaning the user will have to authenticate every time the token expires. This is if I understood the documentation correctly).
So, on the controller I've created to handle the route, I have the following (from an example on Asana's documentation):
require 'omniauth-asana'
use OmniAuth::Strategies::Asana, <secret>, <secret>
creds = request.env["omniauth.auth"]["credentials"].tap { |h| h.delete('expires') }
strategy = request.env["omniauth.strategy"]
access_token = OAuth2::AccessToken.from_hash(strategy.client, creds).refresh!
$client = Asana::Client.new do |c|
c.authentication :oauth2, access_token
end
Now, the above doesn't work because 1) there's no request.env as this is an API only app, so I've followed the instruction on Omniauth and have added the following to my config/application.rb:
config.session_store :cookie_store, key: '_interslice_session'
config.middleware.use ActionDispatch::Cookies # Required for all session management
config.middleware.use ActionDispatch::Session::CookieStore, config.session_options
Now, in the request.headers I have _interslice_session which has some numbers. How can I create a Asana client with the above?
Any ideas?
OK, I think I see what you're attempting to do here; I think the best way forward is to start with how OAuth's Authorization Code Grant happens in general, then move into specifics for OmniAuth.
You send the user to a URL that Asana owns; that is, your goal is to get the user to visit a particular url. For Asana, this is https://app.asana.com/-/oauth_authorize. (Note that we respond with an error if you don't sent a correct client_id param, but feel free to check that link if you want). Do not send the client_secret during this request - it is intended to never be involved in client-side code, as this is insecure.
If they agree to give access, Asana sends back a redirect request to the user's browser with a short-lived code. That then means that your server will be called from the user's browser with this code as a parameter, so has to handle a new incoming request from the browser to whatever you specified as your redirect URI. Also, this location must be accessible by all users of your integration wherever they are.
You send this code from your server as a POST request to https://app.asana.com/-/oauth_token with your client_secret to Asana for a refresh token. This is where your application actually asks for credentials; the token given in the previous phases simply acknowledges that for a short time, the user has given your app permission to ask for these credentials, and your client_secret assures Asana that, for this server-side request, your app really is yours (it's like your application's password).
We send back an access_token which represents (approximately) a client-user credential pair that is valid for an hour.
You use these credentials to access our API on behalf of this user. We also send back a refresh_token which is long-lived, and used to get new short-lived access_tokens after they expire in a very similar way.
OK, so how this works with OmniAuth if I grok it correctly is that it expects to handle almost all of it. I'll be working through our omniauth example in our ruby-asana client library here: https://github.com/Asana/ruby-asana/blob/master/examples/omniauth_integration.rb
You set up OmniAuth with your client id and client secret
use OmniAuth::Strategies::Asana, <client_id>, <client_secret>
A request comes in, and you don't have credentials for it.
get '/' do
if $client
...
else
'sign in to asana'
end
end
The user clicks the sign in link, which (code omitted) sends them to the sign_in endpoint. This endpoint issues a redirect to /auth/asana
The browser requests /auth/asana from our server. If you look at that example, it's not implemented in our code. That's because the /auth/:provider is magically handled by OmniAuth.
This is where all the magic happens. OmniAuth handles the entire login flow above: send browser to our oauth_authorize above, then receive the callback and sticks the relevant params in the environment such that it knows "we just got the short lived code". By the time these lines get hit:
creds = request.env["omniauth.auth"]["credentials"].tap { |h| h.delete('expires') }
strategy = request.env["omniauth.strategy"]
you are inside a callback that OmniAuth has intercepted, gotten the needed creds, and set the creds in the environment. You shouldn't have to handle the oauth callback and token exchange manually.
Now, with the code you provided, I notice a few things right off:
You are causing the popup cycle to happen client side. It may be (and I strongly suspect) that this won't work with OmniAuth - it expects to handle the whole OAuth flow.
Based on the code snippet you provided, you aren't serving this out of a request-response cycle in the controller, rather, it appears that this is in the controller body and not out of an instance method. It may be a typo, but this needs to be in a method that is called back to outside of Rails (that is, a route must point to this a controller method that Asana can use to handle the browser request).
You shouldn't have to look at request.headers, I think - I'm not sure what the issues might be with request.env, but I suspect they may be unrelated to the API-only nature of your app. Are you sure that this is because it's API-only? After adding in the middleware, did you double-check that you can't access request.env? My hunch would be that persistent data in request.env will still be there, only it would require on the middleware being added in to do this. The instructions on OmniAuth simply say that you need to have a session store for your API - which makes sense to me, because APIs don't necessarily need to store state across requests, OmniAuth is telling you to put a session store back in.
I know this is a lot of info, but hopefully it helps you get on the right track. Cheers!

DotNetOpenAuth OAuth2.0 state parameter

I'm using DotNetOpenAuth to connect to Facebook and Google via OAuth2. The OAuth specs ask that no additional parameters be supplied in the request_uri and Google actually enforces this somewhat by forcing to to specify an exact call back uri when you define your Google App with them.
What I want to accomplish is to be able to return the user to a specific URL after they have been authenticated with Facebook or Google. The flow is this, the user clicks on a protected link, they get forwarded to my login page with a returnUrl parameter and then I kick off the authorization process based on the OAuth2 authorization server they choose.
Since the request_uri can't have any parameters in it (though Facebook lets you get away with this), I can't send the returnUrl parameter to the authorization server and get it back such that when the user is returned to my site, I forward them to the protected page they were trying to access. The best I can do is to forward them to the homepage or a member welcome page.
The way to fix this is to use the "state" parameter which the authorization server will send back to the request_uri, but I can't find a way to specify this with DotNetOpenAuth.
By default, it looks like the code uses the SessionID as the state parameter to verify the request coming back from the authorization server. Specifying an IClientAuthorizationTracker on the WebServerClient class lets me plug in my logic when the response is coming back from the authorization server but it's not called when the authorization request is being prepared, so I can't plug in my additional state.
This is code from WebServerClient.cs's PrepareRequestUserAuthorization:
// Mitigate XSRF attacks by including a state value that would be unpredictable between users, but
// verifiable for the same user/session.
// If the host is implementing the authorization tracker though, they're handling this protection themselves.
if (this.AuthorizationTracker == null) {
var context = this.Channel.GetHttpContext();
if (context.Session != null) {
request.ClientState = context.Session.SessionID;
} else {
Logger.OAuth.WarnFormat("No request context discovered, so no client state parameter could be set to mitigate XSRF attacks.");
}
}
There is no else block here which is what I would have expected to be able to play along and plug in my own data.
Any tips on what I'm missing?
The state parameter is necessarily preoccupied with mitigating XSRF attacks. Since you already have a user session, can you just store the returnUrl in the session dictionary?
Alternatively, you can file an issue with DotNetOpenAuth asking that you be allowed to bundle your own data into the state parameter (along with DNOA's own XSRF mitigation code).

Resources