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