I am creating an API for my application. On one side, there is the API server (and the main app) and on the other, the client. The sever uses DoorKeeper to secure the API using OAuth 2.0 (basically turning the main app into an OAuth 2.0 provider) and the client is using OmniAuth with a custom strategy for my app.
The main app uses multitenancy using subdomains; so every client has its own subdomain. There is also a oauth subdomain that is routed to the DoorKeeper interface.
When a user clicks on the "Log in with my app" link, he gets redireced to the oauth subdomain. If he is not logged in to the main app, he needs to get redirected to the login page under the correct subdomain. So I need to pass the client's account name to the server so that DoorKeeper knows to which subdomain to redirect to.
How can I achieve that please?
I've researched on the subject and found out how to pass to OmniAuth params that will get passed to the callback action. Will those params be available to the server?
EDIT: I am not using Devise!
EDIT 2: Here is some code.
Client app session controller create action (log in with my app)
def set_client
self.current_client = Client.find(params[:client][:name])
redirect_to "/auth/catapult?client=#{self.current_client.account_name}"
end
As you can see, I append the client param to the OmniAuth route, but this param is not passed to the server app (DoorKeeper), so I have no idea where to redirect to on the server app.
DoorKeeper config
resource_owner_authenticator do
p params
User.find_by_id(session[:user_id]) || redirect_to(log_in_path)
end
In the redirect above, I need to specify the client's account name as subdomain, but I don't have this info (client's account name) since the params hash does't contain the client's account name that I passed (the client param)
I found out how to fix my problem. I had to dig in deeper into the OmniAuth source code. what I had to do is override the request_phase method in my custom strategy as follow:
def request_phase
redirect client.auth_code.authorize_url({:redirect_uri => callback_url, :catapult_client => request.params["client"]}.merge(authorize_params))
end
Where :catapult_client is, add any extra params you want to pass and it just works!
The simplest way is to pass the place-to-redirect-to-after-authenticating-successfully as a query param when they are redirected to the login page, so it's there as part of the GET request's querystring. Store it, and on a successful auth, redirect them there. You don't need to involve this data in the OAuth process at all.
Of course, I'm assuming that they all start at their subdomain too.
Edit:
When a user clicks on the "Log in with my app" link, he gets redireced to the oauth subdomain.
Assuming the user starts at mysubdomain.yourapp.com, they click on the "Log in with my app" link.
The link also contains a query parameter with the subdomain in it, so oauth.yourapp.com?redirect=mysubdomain.yourapp.com (or just oauth.yourapp.com?redirect=mysubdomain)
The user arrives at oauth.yourapp.com. The app stores the query parameter. The user puts in their details or is redirected to a serviceā¦
The OAuth process is finished, the user has been authenticated.
Redirect the user back to the redirect parameter stored earlier.
This is how I do it, just not with Rails, but I don't see why you couldn't use this process with any framework. This, as I mentioned, depends on the user starting on the correct subdomain.
Related
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.
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.
Since the "Use Strict Mode for Redirect URIs" became obligatory I have an issue with my application. It uses the
user.myapp.com
schema to allow different users to log inte their "work area". But while the list of the users is dynamic, I cannot modify the Client OAuth Settings every time a new user is added.
Is there any solution to this problem?
What I did was to create a dedicated app.myapp.com subdomain, added it to "Valid OAuth Redirect URIs" list and passed it URL to FB login.
But in this case I am getting
Cross-site request forgery validation failed. Required param "state" missing from persistent data.
message (I call FB login from app1.myapp.com, the callback is at app.myapp.com with an intention to redirect back to app1.myapp.com)
Just to clarify: if, for test purposes, I redirect back to app1.myapp.com, everything works great. So the code is correct.
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!
I'm using Devise 3.5 with Omniauth in a Rails 4 app. I've created an integration with Facebook that allows a user to connect their Facebook account to my app. Currently when the user clicks the connect button, they're sent to /user/auth/facebook and then redirected to the callback url that Omniauth generates: /user/auth/facebook/callback. What I'd like to do is manually override this callback url in some cases - meaning that I don't want to override it in an initializer - with a fully qualified url. For example, if a user starts out on http://www.example.com/ I might want to override the default callback url with http://app.example.com/user/auth/facebook/callback.
My app has dynamic subdomains and a user will (almost) always begin the authentication process on a subdomain. Unfortunately it seems that Facebook doesn't support wildcards in oauth redirect urls, which is why I want the ability to detect if a user is on a subdomain and adjust the callback url to something that I have whitelisted on my Facebook app so that the authorization process succeeds.
From what I've read, the url helper omniauth_authorize_path accepts additional arguments to be passed on as parameters. I've tried passing a custom callback path in like so, but without success:
user_omniauth_authorize_path(:facebook, callback_path: #custom_callback)
I've also tried changing callback_path to redirect_url and redirect_uri, but nothing seems to work. When I look at the link that's generated, it does indeed include the callback as a parameter in the url, but when I click the link, I'm redirected back to the default callback url instead of the custom callback url.
Here's how I solved this problem. I'm sure there are other ways, but this seems like the simplest most elegant solution I could come up with.
In config/routes.rb I set up an auth subdomain. All my Oauth connect requests will start on different subdomains and then Facebook is set up to forward those users back to the auth.example.com subdomain.
constraints AuthRedirect do
devise_scope :contact do
get '/auth/facebook/callback' => 'omniauth_callbacks#facebook'
post '/auth/facebook/callback' => 'omniauth_callbacks#facebook'
end
end
Here is /lib/auth_redirect.rb. This just checks if the subdomain is auth and captures that traffic. This is placed at the top of my routes list so as to take precedence over other subdomains.
class AuthRedirect
def self.matches?(request)
request.subdomain.present? && request.subdomain == 'auth'
end
end
Then in my client, when a user clicks the Connect with Facebook button, I send them to /auth/facebook?contact_id=<id>. From here Devise directs them to Facebook, which then redirects them back to https://auth.example.com/.
Then in OmniauthCallbacksController#facebook I can pull the user's id from the omniauth params like so:
auth = env["omniauth.auth"]
contact = Contact.find(env['omniauth.params']['contact_id'])
From here I can persist the credentials to the database and the redirect the user back to the appropriate subdomain. This solution avoids problems with CSRF tokens and more importantly does not require me to use Ruby/ERB to build the omniauth authorize path that the user is sent to when they click the connect button.
have you tried with redirect_uri ?
user_omniauth_authorize_path(:facebook, redirect_uri: #custom_callback)
EDIT: sorry I missed the second part of your post.
I actually have the same problem in production but it works perfectly on a staging environment. The only difference is about the callback url on staging which has one more subdomain *.staging.domain.com
By the way you can provide a static callback_url in the devise initializer file:
config.oaumniauth :facebook, ..., callback_url: 'url right here'
I'm on this issue this yesterday.
Either I provide a static callback url but facebook raises me an CRSF error:
omniauth: (facebook) Authentication failure! csrf_detected: OmniAuth::Strategies::OAuth2::CallbackError, csrf_detected | CSRF detected
Or I let devise set the callback_url dynamically which gonna look like
https://*.domain.com/DEVISE_MODELS/auth/facebook
and in this case I get a straight non matching/whitelisted callback url during FG loggin in process.
EDIT2:
GOOD! I made it. I'm able to get oauth login in with wildcard subdomain.
Provide a static callback_url in your devise initializer
add the domain to your session store as :
domain: ".domain.com"
With that I'm getting neither CRSF error nor nunmatching CB url/whitelisted.
Hope it'll work for you !