|
| 1 | +page.title=Authenticating to OAuth2 Services |
| 2 | +parent.title=Identifying and Authenticating Users |
| 3 | +parent.link=index.html |
| 4 | + |
| 5 | +trainingnavtop=true |
| 6 | +previous.title=Identifying Your User |
| 7 | +previous.link=identify.html |
| 8 | +next.title=Creating a Custom Account Type |
| 9 | +next.link=custom_auth.html |
| 10 | + |
| 11 | +@jd:body |
| 12 | + |
| 13 | + <!-- This is the training bar --> |
| 14 | +<div id="tb-wrapper"> |
| 15 | + <div id="tb"> |
| 16 | +<h2>This lesson teaches you to</h2> |
| 17 | +<ol> |
| 18 | + <li><a href="#Gather">Gather Information</a></li> |
| 19 | + <li><a href="#RequestToken">Request an Auth Token</a></li> |
| 20 | + <li><a href="#RequestAgain">Request an Auth Token... Again</a></li> |
| 21 | + <li><a href="#ConnectToService">Connect to the Online Service</a></li> |
| 22 | +</ol> |
| 23 | + </div> |
| 24 | +</div> |
| 25 | + |
| 26 | +<p>In order to securely access an online service, users need to authenticate to |
| 27 | +the service—they need to provide proof of their identity. For an |
| 28 | +application that accesses a third-party service, the security problem is even |
| 29 | +more complicated. Not only does the user need to be authenticated to access the |
| 30 | +service, but the application also needs to be authorized to act on the user's |
| 31 | +behalf. </p> |
| 32 | + |
| 33 | +<p>The industry standard way to deal with authentication to third-party services |
| 34 | +is the OAuth2 protocol. OAuth2 provides a single value, called an <strong>auth |
| 35 | +token</strong>, that represents both the user's identity and the application's |
| 36 | +authorization to act on the user's behalf. This lesson demonstrates connecting |
| 37 | +to a Google server that supports OAuth2. Although Google services are used as an |
| 38 | +example, the techniques demonstrated will work on any service that correctly |
| 39 | +supports the OAuth2 protocol.</p> |
| 40 | + |
| 41 | +<p>Using OAuth2 is good for:</p> |
| 42 | +<ul> |
| 43 | +<li>Getting permission from the user to access an online service using his or |
| 44 | +her account.</li> |
| 45 | +<li>Authenticating to an online service on behalf of the user.</li> |
| 46 | +<li>Handling authentication errors.</li> |
| 47 | +</ul> |
| 48 | + |
| 49 | + |
| 50 | +<h2 id="Gather">Gather Information</h2> |
| 51 | + |
| 52 | +<p>To begin using OAuth2, you need to know a few things about the API you're trying |
| 53 | +to access:</p> |
| 54 | + |
| 55 | +<ul> |
| 56 | +<li>The url of the service you want to access.</li> |
| 57 | +<li>The <strong>auth scope</strong>, which is a string that defines the specific |
| 58 | +type of access your app is asking for. For instance, the auth scope for |
| 59 | +read-only access to Google Tasks is <code>View your tasks</code>, while the auth |
| 60 | +scope for read-write access to Google Tasks is <code>Manage Your |
| 61 | +Tasks</code>.</li> |
| 62 | +<li>A <strong>client id</strong> and <strong>client secret</strong>, which are |
| 63 | +strings that identify your app to the service. You need to obtain these strings |
| 64 | +directly from the service owner. Google has a self-service system for obtaining |
| 65 | +client ids and secrets. The article <a |
| 66 | +href="http://code.google.com/apis/tasks/articles/oauth-and-tasks-on-android. |
| 67 | +html">Getting Started with the Tasks API and OAuth 2.0 on Android</a> explains |
| 68 | +how to use this system to obtain these values for use with the Google Tasks |
| 69 | +API.</li> |
| 70 | +</ul> |
| 71 | + |
| 72 | + |
| 73 | +<h2 id="RequestToken">Request an Auth Token</h2> |
| 74 | + |
| 75 | +<p>Now you're ready to request an auth token. Auth tokens usually expire after |
| 76 | +some period of time, so you'll have to renew them.</p> |
| 77 | + |
| 78 | + <!-- TODO: I think a flowchart would be useful here, or perhaps a link to an as-yet-to-be-created |
| 79 | +flowchart that lives in the docs. --> |
| 80 | + |
| 81 | +<p>To get an auth token you first need to request the |
| 82 | +{@link android.Manifest.permission#ACCOUNT_MANAGER} |
| 83 | +to yourmanifest file. To actually do anything useful with the |
| 84 | +token, you'll also need to add the {@link android.Manifest.permission#INTERNET} |
| 85 | +permission.</p> |
| 86 | + |
| 87 | +<code> |
| 88 | +<manifest ... > |
| 89 | + <uses-permission android:name="android.permission.ACCOUNT_MANAGER" /> |
| 90 | + <uses-permission android:name="android.permission.INTERNET" /> |
| 91 | + ... |
| 92 | +</manifest> |
| 93 | +</code> |
| 94 | + |
| 95 | + |
| 96 | +<p>Once your app has these permissions set, you can call {@link |
| 97 | +android.accounts.AccountManager#getAuthToken AccountManager.getAuthToken()} to get the |
| 98 | +token.</p> |
| 99 | + |
| 100 | +<p>Watch out! Calling methods on {@link android.accounts.AccountManager} can be tricky! Since |
| 101 | +account operations may involve network communication, most of the {@link |
| 102 | +android.accounts.AccountManager} methods are asynchronous. This means that instead of doing all of |
| 103 | +your auth work in one function, you need to implement it as a series of callbacks. For example:</p> |
| 104 | + |
| 105 | +<pre> |
| 106 | +AccountManager am = AccountManager.get(this); |
| 107 | +Bundle options = new Bundle(); |
| 108 | + |
| 109 | +am.getAuthToken( |
| 110 | + myAccount_, // Account retrieved using getAccountsByType() |
| 111 | + "Manage your tasks", // Auth scope |
| 112 | + options, // Authenticator-specific options |
| 113 | + this, // Your activity |
| 114 | + new OnTokenAcquired(), // Callback called when a token is successfully acquired |
| 115 | + new Handler(new OnError())); // Callback called if an error occurs |
| 116 | +</pre> |
| 117 | + |
| 118 | +<p>In this example, <code>OnTokenAcquired</code> is a class that extends |
| 119 | +{@link android.accounts.AccountManagerCallback}. {@link android.accounts.AccountManager} calls |
| 120 | +{@link android.accounts.AccountManagerCallback#run run()} on <code>OnTokenAcquired</code> with an |
| 121 | +{@link android.accounts.AccountManagerFuture} that contains a {@link android.os.Bundle}. If |
| 122 | +the call succeeded, the token is inside |
| 123 | +the {@link android.os.Bundle}.</p> |
| 124 | + |
| 125 | +<p>Here's how you can get the token from the {@link android.os.Bundle}:</p> |
| 126 | + |
| 127 | +<pre> |
| 128 | +private class OnTokenAcquired implements AccountManagerCallback<Bundle> { |
| 129 | + @Override |
| 130 | + public void run(AccountManagerFuture<Bundle> result) { |
| 131 | + // Get the result of the operation from the AccountManagerFuture. |
| 132 | + Bundle bundle = result.getResult(); |
| 133 | + |
| 134 | + // The token is a named value in the bundle. The name of the value |
| 135 | + // is stored in the constant AccountManager.KEY_AUTHTOKEN. |
| 136 | + token = bundle.getString(AccountManager.KEY_AUTHTOKEN); |
| 137 | + ... |
| 138 | + } |
| 139 | +} |
| 140 | +</pre> |
| 141 | + |
| 142 | +<p>If all goes well, the {@link android.os.Bundle} contains a valid token in the {@link |
| 143 | +android.accounts.AccountManager#KEY_AUTHTOKEN} key and you're off to the races. Things don't |
| 144 | +always go that smoothly, though...</p> |
| 145 | + |
| 146 | + |
| 147 | +<h2 id="RequestAgain">Request an Auth Token... Again</h2> |
| 148 | + |
| 149 | +<p>Your first request for an auth token might fail for several reasons:</p> |
| 150 | + |
| 151 | +<ul> |
| 152 | +<li>An error in the device or network caused {@link android.accounts.AccountManager} to fail.</li> |
| 153 | +<li>The user decided not to grant your app access to the account.</li> |
| 154 | +<li>The stored account credentials aren't sufficient to gain access to the account.</li> |
| 155 | +<li>The cached auth token has expired.</li> |
| 156 | +</ul> |
| 157 | + |
| 158 | +<p>Applications can handle the first two cases trivially, usually by simply |
| 159 | +showing an error message to the user. If the network is down or the user decided |
| 160 | +not to grant access, there's not much that your application can do about it. The |
| 161 | +last two cases are a little more complicated, because well-behaved applications |
| 162 | +are expected to handle these failures automatically.</p> |
| 163 | + |
| 164 | +<p>The third failure case, having insufficient credentials, is communicated via the {@link |
| 165 | +android.os.Bundle} you receive in your {@link android.accounts.AccountManagerCallback} |
| 166 | +(<code>OnTokenAcquired</code> from the previous example). If the {@link android.os.Bundle} includes |
| 167 | +an {@link android.content.Intent} in the {@link android.accounts.AccountManager#KEY_INTENT} key, |
| 168 | +then the authenticator is telling you that it needs to interact directly with the user before it can |
| 169 | +give you a valid token.</p> |
| 170 | + |
| 171 | +<p>There may be many reasons for the authenticator to return an {@link android.content.Intent}. It |
| 172 | +may be the first time the user has logged in to this account. Perhaps the user's account has expired |
| 173 | +and they need to log in again, or perhaps their stored credentials are incorrect. Maybe the account |
| 174 | +requires two-factor authentication or it needs to activate the camera to do a retina scan. It |
| 175 | +doesn't really matter what the reason is. If you want a valid token, you're going to have to fire |
| 176 | +off the {@link android.content.Intent} to get it.</p> |
| 177 | + |
| 178 | +<pre> |
| 179 | +private class OnTokenAcquired implements AccountManagerCallback<Bundle> { |
| 180 | + @Override |
| 181 | + public void run(AccountManagerFuture<Bundle> result) { |
| 182 | + ... |
| 183 | + Intent launch = (Intent) result.get(AccountManager.KEY_INTENT); |
| 184 | + if (launch != null) { |
| 185 | + startActivityForResult(launch, 0); |
| 186 | + return; |
| 187 | + } |
| 188 | + } |
| 189 | +} |
| 190 | +</pre> |
| 191 | + |
| 192 | +<p>Note that the example uses {@link android.app.Activity#startActivityForResult |
| 193 | +startActivityForResult()}, so that you can capture |
| 194 | +the result of the {@link android.content.Intent} by implementing {@link |
| 195 | +android.app.Activity#onActivityResult onActivityResult()} in |
| 196 | +your own activity. This is important! If you don't capture the result from the |
| 197 | +authenticator's response {@link android.content.Intent}, |
| 198 | +it's impossible to tell whether the user has successfully authenticated or not. |
| 199 | +If the result is {@link android.app.Activity#RESULT_OK}, then the |
| 200 | +authenticator has updated the stored credentials so that they are sufficient for |
| 201 | +the level of access you requested, and you should call {@link |
| 202 | +android.accounts.AccountManager#getAuthToken AccountManager.getAuthToken()} again to request the new |
| 203 | +auth token.</p> |
| 204 | + |
| 205 | +<p>The last case, where the token has expired, it is not actually an {@link |
| 206 | +android.accounts.AccountManager} failure. The only way to discover whether a token is expired or not |
| 207 | +is to contact the server, and it would be wasteful and expensive for {@link |
| 208 | +android.accounts.AccountManager} to continually go online to check the state of all of its tokens. |
| 209 | +So this is a failure that can only be detected when an application like yours tries to use the auth |
| 210 | +token to access an online service.</p> |
| 211 | + |
| 212 | + |
| 213 | +<h2 id="ConnectToService">Connect to the Online Service</h2> |
| 214 | + |
| 215 | +<p>The example below shows how to connect to a Google server. Since Google uses the |
| 216 | +industry standard OAuth2 protocol to |
| 217 | +authenticate requests, the techniques discussed here are broadly |
| 218 | +applicable. Keep in mind, though, that every |
| 219 | +server is different. You may find yourself needing to make minor adjustments to |
| 220 | +these instructions to account for your specific |
| 221 | +situation.</p> |
| 222 | + |
| 223 | +<p>The Google APIs require you to supply four values with each request: the API |
| 224 | +key, the client ID, the client secret, |
| 225 | +and the auth key. The first three come from the Google API Console |
| 226 | +website. The last is the string value you |
| 227 | +obtained by calling {@link android.accounts.AccountManager#getAuthToken(android.accounts.Account,java.lang.String,android.os.Bundle,android.app.Activity,android.accounts.AccountManagerCallback,android.os.Handler) AccountManager.getAuthToken()}. You pass these to the |
| 228 | +Google Server as part of |
| 229 | +an HTTP request.</p> |
| 230 | + |
| 231 | +<pre> |
| 232 | +URL url = new URL("https://www.googleapis.com/tasks/v1/users/@me/lists?key=" + <em>your_api_key</em>); |
| 233 | +URLConnection conn = (HttpURLConnection) url.openConnection(); |
| 234 | +conn.addRequestProperty("client_id", <em>your client id</em>); |
| 235 | +conn.addRequestProperty("client_secret", <em>your client secret</em>); |
| 236 | +conn.setRequestProperty("Authorization", "OAuth " + token); |
| 237 | +</pre> |
| 238 | + |
| 239 | +<p>If the request returns |
| 240 | +an HTTP error code of 401, then your token has been denied. As mentioned in the |
| 241 | +last section, the most common reason for |
| 242 | +this is that the token has expired. The fix is |
| 243 | +simple: call |
| 244 | +{@link android.accounts.AccountManager#invalidateAuthToken AccountManager.invalidateAuthToken()} and |
| 245 | +repeat the token acquisition dance one |
| 246 | +more time.</p> |
| 247 | + |
| 248 | +<p>Because expired tokens are such a common occurrence, and fixing them is so easy, many |
| 249 | +applications just assume the token has expired before even asking for it. If renewing a token is a |
| 250 | +cheap operation for your server, you might prefer to call {@link |
| 251 | +android.accounts.AccountManager#invalidateAuthToken AccountManager.invalidateAuthToken()} before the |
| 252 | +first call to {@link android.accounts.AccountManager#getAuthToken AccountManager.getAuthToken()}, |
| 253 | +and spare yourself the need to request an auth token twice.</p> |
0 commit comments