| Scott Main | 98af5edc7 | 2011-12-15 20:13:20 -0800 | [diff] [blame] | 1 | page.title=Authenticating to OAuth2 Services |
| Dirk Dougherty | 3b33cdc | 2011-12-15 15:34:41 -0800 | [diff] [blame] | 2 | parent.title=Remembering and Authenticating Users |
| Scott Main | f9cca66 | 2011-12-15 10:07:03 -0800 | [diff] [blame] | 3 | parent.link=index.html |
| 4 | |
| 5 | trainingnavtop=true |
| Dirk Dougherty | 3b33cdc | 2011-12-15 15:34:41 -0800 | [diff] [blame] | 6 | previous.title=Remembering Your User |
| Scott Main | f9cca66 | 2011-12-15 10:07:03 -0800 | [diff] [blame] | 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 |
| Scott Main | cd1b08e | 2011-12-27 16:22:27 -0800 | [diff] [blame] | 66 | href="http://code.google.com/apis/tasks/articles/oauth-and-tasks-on-android.html">Getting |
| 67 | Started with the Tasks API and OAuth 2.0 on Android</a> explains |
| Scott Main | f9cca66 | 2011-12-15 10:07:03 -0800 | [diff] [blame] | 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 | |
| Dirk Dougherty | 3b33cdc | 2011-12-15 15:34:41 -0800 | [diff] [blame] | 75 | <p>Now you're ready to request an auth token. This is a multi-step process.</p> |
| Scott Main | f9cca66 | 2011-12-15 10:07:03 -0800 | [diff] [blame] | 76 | |
| Dirk Dougherty | 3b33cdc | 2011-12-15 15:34:41 -0800 | [diff] [blame] | 77 | <img src="{@docRoot}images/training/oauth_dance.png" alt="Procedure for obtaining |
| 78 | a valid auth token from the Android Account Manager"/> |
| Scott Main | f9cca66 | 2011-12-15 10:07:03 -0800 | [diff] [blame] | 79 | |
| 80 | <p>To get an auth token you first need to request the |
| 81 | {@link android.Manifest.permission#ACCOUNT_MANAGER} |
| Dirk Dougherty | dd93ae3 | 2013-09-09 10:36:37 -0700 | [diff] [blame] | 82 | to your manifest file. To actually do anything useful with the |
| Scott Main | f9cca66 | 2011-12-15 10:07:03 -0800 | [diff] [blame] | 83 | token, you'll also need to add the {@link android.Manifest.permission#INTERNET} |
| 84 | permission.</p> |
| 85 | |
| Dirk Dougherty | 3b33cdc | 2011-12-15 15:34:41 -0800 | [diff] [blame] | 86 | <pre> |
| Scott Main | f9cca66 | 2011-12-15 10:07:03 -0800 | [diff] [blame] | 87 | <manifest ... > |
| 88 | <uses-permission android:name="android.permission.ACCOUNT_MANAGER" /> |
| 89 | <uses-permission android:name="android.permission.INTERNET" /> |
| 90 | ... |
| 91 | </manifest> |
| Dirk Dougherty | 3b33cdc | 2011-12-15 15:34:41 -0800 | [diff] [blame] | 92 | </pre> |
| Scott Main | f9cca66 | 2011-12-15 10:07:03 -0800 | [diff] [blame] | 93 | |
| 94 | |
| 95 | <p>Once your app has these permissions set, you can call {@link |
| 96 | android.accounts.AccountManager#getAuthToken AccountManager.getAuthToken()} to get the |
| 97 | token.</p> |
| 98 | |
| 99 | <p>Watch out! Calling methods on {@link android.accounts.AccountManager} can be tricky! Since |
| 100 | account operations may involve network communication, most of the {@link |
| 101 | android.accounts.AccountManager} methods are asynchronous. This means that instead of doing all of |
| 102 | your auth work in one function, you need to implement it as a series of callbacks. For example:</p> |
| 103 | |
| 104 | <pre> |
| 105 | AccountManager am = AccountManager.get(this); |
| 106 | Bundle options = new Bundle(); |
| 107 | |
| 108 | am.getAuthToken( |
| 109 | myAccount_, // Account retrieved using getAccountsByType() |
| 110 | "Manage your tasks", // Auth scope |
| 111 | options, // Authenticator-specific options |
| 112 | this, // Your activity |
| 113 | new OnTokenAcquired(), // Callback called when a token is successfully acquired |
| 114 | new Handler(new OnError())); // Callback called if an error occurs |
| 115 | </pre> |
| 116 | |
| Scott Main | a15afd2 | 2013-03-12 09:25:22 -0700 | [diff] [blame] | 117 | <p>In this example, <code>OnTokenAcquired</code> is a class that implements |
| Scott Main | f9cca66 | 2011-12-15 10:07:03 -0800 | [diff] [blame] | 118 | {@link android.accounts.AccountManagerCallback}. {@link android.accounts.AccountManager} calls |
| 119 | {@link android.accounts.AccountManagerCallback#run run()} on <code>OnTokenAcquired</code> with an |
| 120 | {@link android.accounts.AccountManagerFuture} that contains a {@link android.os.Bundle}. If |
| 121 | the call succeeded, the token is inside |
| 122 | the {@link android.os.Bundle}.</p> |
| 123 | |
| 124 | <p>Here's how you can get the token from the {@link android.os.Bundle}:</p> |
| 125 | |
| 126 | <pre> |
| 127 | private class OnTokenAcquired implements AccountManagerCallback<Bundle> { |
| 128 | @Override |
| 129 | public void run(AccountManagerFuture<Bundle> result) { |
| 130 | // Get the result of the operation from the AccountManagerFuture. |
| 131 | Bundle bundle = result.getResult(); |
| 132 | |
| 133 | // The token is a named value in the bundle. The name of the value |
| 134 | // is stored in the constant AccountManager.KEY_AUTHTOKEN. |
| 135 | token = bundle.getString(AccountManager.KEY_AUTHTOKEN); |
| 136 | ... |
| 137 | } |
| 138 | } |
| 139 | </pre> |
| 140 | |
| 141 | <p>If all goes well, the {@link android.os.Bundle} contains a valid token in the {@link |
| 142 | android.accounts.AccountManager#KEY_AUTHTOKEN} key and you're off to the races. Things don't |
| 143 | always go that smoothly, though...</p> |
| 144 | |
| 145 | |
| 146 | <h2 id="RequestAgain">Request an Auth Token... Again</h2> |
| 147 | |
| 148 | <p>Your first request for an auth token might fail for several reasons:</p> |
| 149 | |
| 150 | <ul> |
| 151 | <li>An error in the device or network caused {@link android.accounts.AccountManager} to fail.</li> |
| 152 | <li>The user decided not to grant your app access to the account.</li> |
| 153 | <li>The stored account credentials aren't sufficient to gain access to the account.</li> |
| 154 | <li>The cached auth token has expired.</li> |
| 155 | </ul> |
| 156 | |
| 157 | <p>Applications can handle the first two cases trivially, usually by simply |
| 158 | showing an error message to the user. If the network is down or the user decided |
| 159 | not to grant access, there's not much that your application can do about it. The |
| 160 | last two cases are a little more complicated, because well-behaved applications |
| 161 | are expected to handle these failures automatically.</p> |
| 162 | |
| 163 | <p>The third failure case, having insufficient credentials, is communicated via the {@link |
| 164 | android.os.Bundle} you receive in your {@link android.accounts.AccountManagerCallback} |
| 165 | (<code>OnTokenAcquired</code> from the previous example). If the {@link android.os.Bundle} includes |
| 166 | an {@link android.content.Intent} in the {@link android.accounts.AccountManager#KEY_INTENT} key, |
| 167 | then the authenticator is telling you that it needs to interact directly with the user before it can |
| 168 | give you a valid token.</p> |
| 169 | |
| 170 | <p>There may be many reasons for the authenticator to return an {@link android.content.Intent}. It |
| 171 | may be the first time the user has logged in to this account. Perhaps the user's account has expired |
| 172 | and they need to log in again, or perhaps their stored credentials are incorrect. Maybe the account |
| 173 | requires two-factor authentication or it needs to activate the camera to do a retina scan. It |
| 174 | doesn't really matter what the reason is. If you want a valid token, you're going to have to fire |
| 175 | off the {@link android.content.Intent} to get it.</p> |
| 176 | |
| 177 | <pre> |
| 178 | private class OnTokenAcquired implements AccountManagerCallback<Bundle> { |
| 179 | @Override |
| 180 | public void run(AccountManagerFuture<Bundle> result) { |
| 181 | ... |
| Trevor Johns | 58a54cb | 2013-03-06 14:46:47 -0800 | [diff] [blame] | 182 | Intent launch = (Intent) result.getResult().get(AccountManager.KEY_INTENT); |
| Scott Main | f9cca66 | 2011-12-15 10:07:03 -0800 | [diff] [blame] | 183 | if (launch != null) { |
| 184 | startActivityForResult(launch, 0); |
| 185 | return; |
| 186 | } |
| 187 | } |
| 188 | } |
| 189 | </pre> |
| 190 | |
| 191 | <p>Note that the example uses {@link android.app.Activity#startActivityForResult |
| 192 | startActivityForResult()}, so that you can capture |
| 193 | the result of the {@link android.content.Intent} by implementing {@link |
| 194 | android.app.Activity#onActivityResult onActivityResult()} in |
| 195 | your own activity. This is important! If you don't capture the result from the |
| 196 | authenticator's response {@link android.content.Intent}, |
| 197 | it's impossible to tell whether the user has successfully authenticated or not. |
| 198 | If the result is {@link android.app.Activity#RESULT_OK}, then the |
| 199 | authenticator has updated the stored credentials so that they are sufficient for |
| 200 | the level of access you requested, and you should call {@link |
| 201 | android.accounts.AccountManager#getAuthToken AccountManager.getAuthToken()} again to request the new |
| 202 | auth token.</p> |
| 203 | |
| 204 | <p>The last case, where the token has expired, it is not actually an {@link |
| 205 | android.accounts.AccountManager} failure. The only way to discover whether a token is expired or not |
| 206 | is to contact the server, and it would be wasteful and expensive for {@link |
| 207 | android.accounts.AccountManager} to continually go online to check the state of all of its tokens. |
| 208 | So this is a failure that can only be detected when an application like yours tries to use the auth |
| 209 | token to access an online service.</p> |
| 210 | |
| 211 | |
| 212 | <h2 id="ConnectToService">Connect to the Online Service</h2> |
| 213 | |
| 214 | <p>The example below shows how to connect to a Google server. Since Google uses the |
| 215 | industry standard OAuth2 protocol to |
| 216 | authenticate requests, the techniques discussed here are broadly |
| 217 | applicable. Keep in mind, though, that every |
| 218 | server is different. You may find yourself needing to make minor adjustments to |
| 219 | these instructions to account for your specific |
| 220 | situation.</p> |
| 221 | |
| 222 | <p>The Google APIs require you to supply four values with each request: the API |
| 223 | key, the client ID, the client secret, |
| 224 | and the auth key. The first three come from the Google API Console |
| 225 | website. The last is the string value you |
| 226 | 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 |
| 227 | Google Server as part of |
| 228 | an HTTP request.</p> |
| 229 | |
| 230 | <pre> |
| 231 | URL url = new URL("https://www.googleapis.com/tasks/v1/users/@me/lists?key=" + <em>your_api_key</em>); |
| 232 | URLConnection conn = (HttpURLConnection) url.openConnection(); |
| 233 | conn.addRequestProperty("client_id", <em>your client id</em>); |
| 234 | conn.addRequestProperty("client_secret", <em>your client secret</em>); |
| 235 | conn.setRequestProperty("Authorization", "OAuth " + token); |
| 236 | </pre> |
| 237 | |
| 238 | <p>If the request returns |
| 239 | an HTTP error code of 401, then your token has been denied. As mentioned in the |
| 240 | last section, the most common reason for |
| 241 | this is that the token has expired. The fix is |
| 242 | simple: call |
| 243 | {@link android.accounts.AccountManager#invalidateAuthToken AccountManager.invalidateAuthToken()} and |
| 244 | repeat the token acquisition dance one |
| 245 | more time.</p> |
| 246 | |
| 247 | <p>Because expired tokens are such a common occurrence, and fixing them is so easy, many |
| 248 | applications just assume the token has expired before even asking for it. If renewing a token is a |
| 249 | cheap operation for your server, you might prefer to call {@link |
| 250 | android.accounts.AccountManager#invalidateAuthToken AccountManager.invalidateAuthToken()} before the |
| 251 | first call to {@link android.accounts.AccountManager#getAuthToken AccountManager.getAuthToken()}, |
| 252 | and spare yourself the need to request an auth token twice.</p> |