I have set up third party OAuth authentication on my Node.js server using passport.js.
It works well from browser. The flow is the following:
Get request to myserver.com/auth/instagram
Passport redirects me to instagram website to login
I type my credentials
I am redirected to the callback url, which is my server.com/auth/instagram/callback
I do further processing and finally res.send(myAccessToken) to the client.
I am having troubles implementing this in Android. To type third-party credential, a browser page will be needed. But then how am I going to get the final result in my app?
Option 1
Start the browser.
Intent i = new Intent(ACTION_VIEW);
i.setData(Uri.parse("https://myserver.com/auth/instagram"));
startActivity(i);
Issues:
I cannot easily set proper headers needed for my server.
I cannot programmatically read the final response! The action is given to the browser app, which goes to the end and will never notify my app. The user is shown a blank page with myAccessToken printed.
Option 2
Connect using some http library.
final Request request = createRequest()
.url("https://myserver.com/auth/instagram")
.get()
.build();
Response response = client.newCall(request).execute();
Issues:
Does not work! Even if I set okhttp to follow redirects, I get a 200 response pointing to some instagram url. Of course the user is never prompted his instagram credentials, because no browser ever opened.
How to deal with this?
Notes:
I have used instagram as an example, the third party service is not relevant to the question. I am looking for a general solution to receive the result callback in my Android app.
I am not willing to implement client-side auth. I am willing to trigger steps 1.-5. from my Android app and receive the final result in a Java callback.
Related
I'm following this documentation to implement OAuth2.0 in my flutter app and don't understand a few things, here is the code from the documentation:
import 'dart:io';
import 'package:oauth2/oauth2.dart' as oauth2;
// These URLs are endpoints that are provided by the authorization
// server. They're usually included in the server's documentation of its
// OAuth2 API.
final authorizationEndpoint =
Uri.parse("http://example.com/oauth2/authorization");
final tokenEndpoint =
Uri.parse("http://example.com/oauth2/token");
// The authorization server will issue each client a separate client
// identifier and secret, which allows the server to tell which client
// is accessing it. Some servers may also have an anonymous
// identifier/secret pair that any client may use.
//
// Note that clients whose source code or binary executable is readily
// available may not be able to make sure the client secret is kept a
// secret. This is fine; OAuth2 servers generally won't rely on knowing
// with certainty that a client is who it claims to be.
final identifier = "my client identifier";
final secret = "my client secret";
// This is a URL on your application's server. The authorization server
// will redirect the resource owner here once they've authorized the
// client. The redirection will include the authorization code in the
// query parameters.
final redirectUrl = Uri.parse("http://my-site.com/oauth2-redirect");
/// A file in which the users credentials are stored persistently. If the server
/// issues a refresh token allowing the client to refresh outdated credentials,
/// these may be valid indefinitely, meaning the user never has to
/// re-authenticate.
final credentialsFile = new File("~/.myapp/credentials.json");
/// Either load an OAuth2 client from saved credentials or authenticate a new
/// one.
Future<oauth2.Client> getClient() async {
var exists = await credentialsFile.exists();
// If the OAuth2 credentials have already been saved from a previous run, we
// just want to reload them.
if (exists) {
var credentials = new oauth2.Credentials.fromJson(
await credentialsFile.readAsString());
return new oauth2.Client(credentials,
identifier: identifier, secret: secret);
}
// If we don't have OAuth2 credentials yet, we need to get the resource owner
// to authorize us. We're assuming here that we're a command-line application.
var grant = new oauth2.AuthorizationCodeGrant(
identifier, authorizationEndpoint, tokenEndpoint,
secret: secret);
// Redirect the resource owner to the authorization URL. This will be a URL on
// the authorization server (authorizationEndpoint with some additional query
// parameters). Once the resource owner has authorized, they'll be redirected
// to `redirectUrl` with an authorization code.
//
// `redirect` is an imaginary function that redirects the resource
// owner's browser.
await redirect(grant.getAuthorizationUrl(redirectUrl));
// Another imaginary function that listens for a request to `redirectUrl`.
var request = await listen(redirectUrl);
// Once the user is redirected to `redirectUrl`, pass the query parameters to
// the AuthorizationCodeGrant. It will validate them and extract the
// authorization code to create a new Client.
return await grant.handleAuthorizationResponse(request.uri.queryParameters);
}
main() async {
var client = await loadClient();
// Once you have a Client, you can use it just like any other HTTP client.
var result = client.read("http://example.com/protected-resources.txt");
// Once we're done with the client, save the credentials file. This ensures
// that if the credentials were automatically refreshed while using the
// client, the new credentials are available for the next run of the
// program.
await credentialsFile.writeAsString(client.credentials.toJson());
print(result);
}
Where can I find the identifier and secret ? Is it shown in the /.well-known/openid-configuration page ? Also how do I implement these functions:
await redirect(grant.getAuthorizationUrl(redirectUrl));
var request = await listen(redirectUrl);
var client = await loadClient();
The documentation mentions that it is an imaginary function. How do I implement those functions?
OAuth with flutter is never going to be completely straight-forward on Android or iOS because it lacks deep integration with the OS, so you'll have to do a bit of per-OS configuration. And to be completely honest, it's not all that easy in native Android/iOS either.
And that plugin you're looking at seems much more focused towards a server application, which is why it doesn't make complete sense to a flutter developer. However, it isn't impossible to use it!
The main thing that enables OAuth to work is using either a Custom Url Scheme or a Universal Link. A Custom Url Scheme is something like com.myapp.customurlscheme:// - it's used instead of 'https'. A Universal link uses https and a website i.e. https://myapp.com/customurl/. An important difference is that to use a Universal link, you must control the website and upload a file that apple can check to know you've given the app permission to replace that website or that part of the website. If the user has the app installed, they will be shown it when they go to that url; if they don't, they'll be shown something by the website (normally a link to install the app).
In the case where you're an authenticating client with OAuth, you normally don't want to be replicating part of a website as all you're doing is making a callback (redirect) url, so you'll probably be using a custom url scheme. This has to be done by adding to either your AndroidManifest.xml or Info.plist files.
For iOS's Info.plist that looks something like:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>[ANY_URL_NAME]</string>
<key>CFBundleURLSchemes</key>
<array>
<string>[YOUR_SCHEME]</string>
</array>
</dict>
</array>
And for the AndroidManifest.xml something like:
<activity>
...
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="[YOUR_SCHEME]"
android:host="[YOUR_HOST]" />
</intent-filter>
Once you have that set up, you can add listeners natively for when the app is 'opened' with one of those custom URLs. That's a bit of a pain, but thankfully someone has made a plugin that helps: Universal Links (and credit to them for the sample config above as I shamelessly stole it from their documentation). You can use its getInitialLink() method in your main function (or somewhere like it) and/or get a stream of links to listen for using getLinksStream(). I think that the second one is what you'll be using since the app is open already when you start the OAuth/OpenID workflow - so you'll start listening either right after the app is opened, or right before you start with the OAuth call.
Okay, so that was a lot. There's a reason for it though - what this has done is made a way for your app to receive redirects from a browser or other app. So if you handle that getLinksStream, you can then more or less receive a callback from the oauth2 server. You could set up some system where you create a future that waits for a particular link being passed through the linkstream.
So that covers
// Another imaginary function that listens for a request to 'redirectUrl'.
var request = await listen(redirectUrl);
Now we need to do something about that first imaginary function. Turns out in the app case, it's not imaginary at all - you need to launch that URL rather than having the page redirect as it would on a server. There's a plugin for that: Url Launcher
So where it's saying await redirect(grant.getAuthorizationUrl(redirectUrl));, what you actually want to be doing is launching grant.getAuthorizationUrl with url_launcher's launch() (with appropriate flags, that you'll have to figure out by testing. You may want to force using a browser, or not, depending on whether the OAuth server has an app that can handle auth for it. If they do, you probably want the url to open up in their app so the user is already logged in).
There's a couple more pieces to this puzzle that need to be fit in. The first is the redirectUrl that you have to pass into getAuthorizationUrl. What do I put in there, you ask?! Well, you use that nifty custom app scheme we set up earlier! So the redirect url will be something like com.myapp.customurlscheme://customurlredirect.
So the OAuth token provisioning workflow goes something like this:
you launch the auth url for the server
a login page is shown to the user, or a permissions page, or whatever the server does
once the user has approved the request, the server redirects the user back to your app (it might ask them "Do you want to open in " or something like it).
Your app receives the callback with the authorization code
Your app should request tokens using that authorization code (I assume that's handled by handleAuthorizationResponse).
Now, before you implement all that, there are a few things to think about.
If this were a server application, you could have a secure secret which proves to the OAuth server that your application is the client it claims to be. You would get that from the OAuth server and provision it directly to the server. However, because you're writing an app there is no (easy) way to provision that secure secret. So instead of using the normal OAuth you should be the OAuth authorization code flow with PKCE, and no client_secret. If that went over your head, you should do some reading up on PKCE - Auth0 has a good writeup. The OAuth server you're working with also has to support that, but if you do this without it your login process will be insecure.
The OAuth server you're communicating with has to both understand and accept custom url schemes. Most of the big ones do and they actually have documentation similar to this which should walk you through the same process (but not flutter-specific). And in fact they actually define what the custom url schemes should be - in Facebook's case if your app id is 1234567 then the custom url scheme would be something like fb1234567://.
I haven't looked that much into that library you're using, but you may want to make sure it actually supports the right OAuth workflow. If it designed as a server-side package as I suspect, it might not. In that case you might have to manually do the setup - which realistically isn't that difficult, you just have to generate a couple of URLs to match what the OAuth server expects which is pretty well documented and standardized.
That was a lot of information, but unfortunately OAuth2 isn't all that simple (and realistically, it can't be all that much simpler if it is to do what it needs to do). Good luck with your app!
I have an application where authentication happens through a passive client. Basically based on server information, a browser will be launched and it will show a login screen. Once the user enters login credentials, the further handling of cookies and session is done in shouldOverrideUrlLoading.
The issue is coming with authentication when I am connecting to web application servers in a clustered environment. When user connects to first server, it shows him login screen and user enters the details, server authenticates, but during session handling in shouldOverrideUrlLoading, my code connects to the same server with same url, but the response from the server comes that user has not been authenticated, while he has already done authentication.
So to differentiate between different servers, we use JSESSIONID to identify server.
I get the original JSESSIONID that was used on the first URL, but when the second URL is fired, my code use JSESSIONID and other cookies from the first URL in the request of second URL. To fire second URL, i use org.apache.http.impl.client.DefaultHttpClient.execute method.
I am not sure what I am missing to get the response from server that user is already authenticated.
I resolved this issue. There was an issue with cookie version, I was using while building a HTTP context for second request.
BasicClientCookie cookie = new BasicClientCookie(name,value);
// cookie.setVersion(1);
cookie.setDomain(host);
cookie.setPath("/");
cookie.setSecure(true);
cookieJar.addCookie(cookie);
I commented version for cookie and then it recognized request to send to same cluster member which was authenticated in first request.
Using the Instagram rubygem and sinatra I am able to successfully authenticate via oauth from a small AngularJS app and use the Instagram API. Here are the relevant oauth routes from my sinatra app:
get '/oauth/connect' do
logger.info "going here " + (Instagram.authorize_url :redirect_uri => CALLBACK)
redirect Instagram.authorize_url :redirect_uri => CALLBACK
end
get "/oauth/callback" do
logger.info params[:code]
response = Instagram.get_access_token params[:code], :redirect_uri => CALLBACK
session[:access_token] = response.access_token
users = DB[:users]
current_user = users[:instagram_id => response.user.id.to_i]
if current_user.nil?
new_user = response.user
users.insert :instagram_id => new_user.id.to_i,
:username => new_user.username,
:profile_picture => new_user.profile_picture,
:created_at => DateTime.now
current_user = users[:instagram_id => new_user.id]
end
session[:current_user] = current_user
redirect "/app"
end
However, when trying to authenticate from within a webview in an Android app using the same routes (GET oauth/connect, redirected to Instagram, then redirected back to oauth/callback) I get the following error:
Instagram::BadRequest - POST https://api.instagram.com/oauth/access_token/: 400: OAuthException: No matching code found.:
I have verified that I am using the same code returned from Instagram and that the redirect_uri is the same as what I registered with Instagram.
Note, Oauth works when I use my web interface, but not via the Android webview.
I have read a number of posts where other people have seen this behavior, but the solutions provided do not work for me:
Instagram can disable your app because it thinks it is misbehaving. Nope, because oauth works via my web-interface still using the same client ID and secret and the same routes.
You are not using the same redirect_uri as that listed with Instagram. I have checked and re-checked this and it looks fine.
Where can I debug next to get to the bottom of this issue?
I know my answer is a bit late, but I'm posting it here so others (hopefully) won't have to search as hard as I did.
It looks like you're using the Explicit auth. We were having this issue using the explicit flow.
Here's the problem: In the instant after you send the code to Instagram (step 3), that code expires. However, Instagram will not send you a new code (step 1 & 2) for at least 5 minutes (it re-issues the old code).
Make certain that you aren't sending more than one request with the same code (Android WebView tends to arbitrarily send multiple requests). Or, better yet, cache the code->Instagram_response mapping on the server.
So I want to login to a webpage which uses Basic Authentication to login.
If I send HTTP Get with the Authorization Header, I get the successful response as HTML. But afterwards, it asks me again, to login, because it didn’t remember that I’m already logged in.
So my question is:
How can I save this information, that I have already been logged in?
I know, I already asked that question before, but since no one answered any more on the other thread my last hope was to open another.
The other thread: Android doesnt notice when i do an WWW-Authenticate
EDIT:
So what i wanna do is:
Intent browserIntent = new Intent(Intent.ACTION_VIEW,Uri.parse("The Website with the Authentication")); // Here, i want to add the request header
handler.startActivity(browserIntent);
But i have no idea how, because, the only thing you can send the intent is the Url (and no request headers).
Hope this makes things clearer.
Basic authentication doesn’t save state on the server end. Every request requires that you include the login information.
What happens normally is that:
The browser makes a request for a URL, lacking any authentication.
The server returns code 401, that this URL requires basic authentication.
The browser asks (either the user, or some database of its own) for the username and password. If it asked the user for the password, the browser then stores the username/password in an internal database, keying it against this URL.
For every further request for this URL, the browser includes that username and password in the HTTP GET request.
So the basic answer is, the server doesn’t remember that you have already logged in. The browser remembers that. Since you’re writing the browser, it’s up to you to have your application remember the username/password combination for that URL. Whatever you are doing now after you receive a 401 response, you simply always do for that URL. You don’t even need to wait for the 401 response.
I am trying to set up an Android app where I can access URL's behind arbitrary proxies or HTTP authentications. That is, the app won't know immediately if a URL needs authentication and will repond to an authentication request by asking the user for credentials, the same way it does for the Android Browser.
I recently worked out how to request user authentication for a WebView, responding to authentication requests from the browser, bringing up a dialog, and advancing the user to the destination page. I am looking to do the same with HttpClient.
The basic process, as I see it, is to:
Perform the request via HttpClient.execute.
If not 401 or 407 with proper headers, trigger a "done" callback.
Otherwise...
Pop up a dialog asking for the username and password.
Set the http credentials in the HTTP client via HttpClient.getCredentialsProvider().setCredentials.
Return to step 1 until the the user clicks cancel or the server gives up. In that case, trigger a "failed" callback with the last response received.
Am I on the right track or has someone already implemented something similar? I would hate to be reinventing the wheel on this.
You should try the authentication example on the apache site
httpclient.getCredentialsProvider().setCredentials(
new AuthScope("localhost", 443),
new UsernamePasswordCredentials("username", "password"));
The direct link to the java file is http://hc.apache.org/httpcomponents-client-ga/httpclient/examples/org/apache/http/examples/client/ClientAuthentication.java
The reactive approach described above did work. Responding to an arbitrary 401 or 407 request is effective with the caveat that you need to suppor each authentication scheme you expect to encounter so something like UsernamePasswordCredentials won't work for NTLM.