1 : /******************************************************************************
2 : * $Id$
3 : *
4 : * Project: Common Portability Library
5 : * Purpose: Google OAuth2 Authentication Services
6 : * Author: Frank Warmerdam, warmerdam@pobox.com
7 : *
8 : ******************************************************************************
9 : * Copyright (c) 2013, Frank Warmerdam
10 : *
11 : * Permission is hereby granted, free of charge, to any person obtaining a
12 : * copy of this software and associated documentation files (the "Software"),
13 : * to deal in the Software without restriction, including without limitation
14 : * the rights to use, copy, modify, merge, publish, distribute, sublicense,
15 : * and/or sell copies of the Software, and to permit persons to whom the
16 : * Software is furnished to do so, subject to the following conditions:
17 : *
18 : * The above copyright notice and this permission notice shall be included
19 : * in all copies or substantial portions of the Software.
20 : *
21 : * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
22 : * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23 : * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
24 : * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25 : * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26 : * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
27 : * DEALINGS IN THE SOFTWARE.
28 : ****************************************************************************/
29 :
30 : #include "cpl_http.h"
31 :
32 : CPL_CVSID("$Id$");
33 :
34 : /* ==================================================================== */
35 : /* Values related to OAuth2 authorization to use fusion */
36 : /* tables. Many of these values are related to the */
37 : /* gdalautotest@gmail.com account for GDAL managed by Even */
38 : /* Rouault and Frank Warmerdam. Some information about OAuth2 */
39 : /* as managed by that account can be found at the following url */
40 : /* when logged in as gdalautotest@gmail.com: */
41 : /* */
42 : /* https://code.google.com/apis/console/#project:265656308688:access*/
43 : /* */
44 : /* Applications wanting to use their own client id and secret */
45 : /* can set the following configuration options: */
46 : /* - GOA2_CLIENT_ID */
47 : /* - GOA2_CLIENT_SECRET */
48 : /* ==================================================================== */
49 : #define GDAL_CLIENT_ID "265656308688.apps.googleusercontent.com"
50 : #define GDAL_CLIENT_SECRET "0IbTUDOYzaL6vnIdWTuQnvLz"
51 :
52 : #define GOOGLE_AUTH_URL "https://accounts.google.com/o/oauth2"
53 :
54 : /************************************************************************/
55 : /* ParseSimpleJson() */
56 : /* */
57 : /* Return a string list of name/value pairs extracted from a */
58 : /* JSON doc. The Google OAuth2 web service returns simple JSON */
59 : /* responses. The parsing as done currently is very fragile */
60 : /* and depends on JSON documents being in a very very simple */
61 : /* form. */
62 : /************************************************************************/
63 :
64 7 : static CPLStringList ParseSimpleJson(const char *pszJson)
65 :
66 : {
67 : /* -------------------------------------------------------------------- */
68 : /* We are expecting simple documents like the following with no */
69 : /* heirarchy or complex structure. */
70 : /* -------------------------------------------------------------------- */
71 : /*
72 : {
73 : "access_token":"1/fFBGRNJru1FQd44AzqT3Zg",
74 : "expires_in":3920,
75 : "token_type":"Bearer"
76 : }
77 : */
78 :
79 : CPLStringList oWords(
80 7 : CSLTokenizeString2(pszJson, " \n\t,:{}", CSLT_HONOURSTRINGS ));
81 7 : CPLStringList oNameValue;
82 :
83 28 : for( int i=0; i < oWords.size(); i += 2 )
84 : {
85 21 : oNameValue.SetNameValue( oWords[i], oWords[i+1] );
86 : }
87 :
88 7 : return oNameValue;
89 : }
90 :
91 : /************************************************************************/
92 : /* GOA2GetAuthorizationURL() */
93 : /************************************************************************/
94 :
95 : /**
96 : * Return authorization url for a given scope.
97 : *
98 : * Returns the URL that a user should visit, and use for authentication
99 : * in order to get an "auth token" indicating their willingness to use a
100 : * service.
101 : *
102 : * Note that when the user visits this url they will be asked to login
103 : * (using a google/gmail/etc) account, and to authorize use of the
104 : * requested scope for the application "GDAL/OGR". Once they have done
105 : * so, they will be presented with a lengthy string they should "enter
106 : * into their application". This is the "auth token" to be passed to
107 : * GOA2GetRefreshToken(). The "auth token" can only be used once.
108 : *
109 : * This function should never fail.
110 : *
111 : * @param pszScope the service being requested, not yet URL encoded, such as
112 : * "https://www.googleapis.com/auth/fusiontables".
113 : *
114 : * @return the URL to visit - should be freed with CPLFree().
115 : */
116 :
117 0 : char *GOA2GetAuthorizationURL(const char *pszScope)
118 :
119 : {
120 0 : CPLString osScope;
121 0 : CPLString osURL;
122 :
123 0 : osScope.Seize(CPLEscapeString(pszScope, -1, CPLES_URL));
124 : osURL.Printf( "%s/auth?scope=%s&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&client_id=%s",
125 : GOOGLE_AUTH_URL,
126 : osScope.c_str(),
127 0 : CPLGetConfigOption("GOA2_CLIENT_ID", GDAL_CLIENT_ID));
128 0 : return CPLStrdup(osURL);
129 : }
130 :
131 : /************************************************************************/
132 : /* GOA2GetRefreshToken() */
133 : /************************************************************************/
134 :
135 : /**
136 : * Turn Auth Token into a Refresh Token.
137 : *
138 : * A one time "auth token" provided by the user is turned into a
139 : * reusable "refresh token" using a google oauth2 web service.
140 : *
141 : * A CPLError will be reported if the translation fails for some reason.
142 : * Common reasons include the auth token already having been used before,
143 : * it not being appropriate for the passed scope and configured client api
144 : * or http connection problems. NULL is returned on error.
145 : *
146 : * @param pszAuthToken the authorization token from the user.
147 : * @param pszScope the scope for which it is valid.
148 : *
149 : * @return refresh token, to be freed with CPLFree(), null on failure.
150 : */
151 :
152 0 : char CPL_DLL *GOA2GetRefreshToken( const char *pszAuthToken,
153 : const char *pszScope )
154 :
155 : {
156 : /* -------------------------------------------------------------------- */
157 : /* Prepare request. */
158 : /* -------------------------------------------------------------------- */
159 0 : CPLString osItem;
160 0 : CPLStringList oOptions;
161 :
162 : oOptions.AddString(
163 0 : "HEADERS=Content-Type: application/x-www-form-urlencoded" );
164 :
165 : osItem.Printf(
166 : "POSTFIELDS="
167 : "code=%s"
168 : "&client_id=%s"
169 : "&client_secret=%s"
170 : "&redirect_uri=urn:ietf:wg:oauth:2.0:oob"
171 : "&grant_type=authorization_code",
172 : pszAuthToken,
173 : CPLGetConfigOption("GOA2_CLIENT_ID", GDAL_CLIENT_ID),
174 0 : CPLGetConfigOption("GOA2_CLIENT_SECRET", GDAL_CLIENT_SECRET));
175 0 : oOptions.AddString(osItem);
176 :
177 : /* -------------------------------------------------------------------- */
178 : /* Submit request by HTTP. */
179 : /* -------------------------------------------------------------------- */
180 : CPLHTTPResult * psResult =
181 0 : CPLHTTPFetch( GOOGLE_AUTH_URL "/token", oOptions);
182 :
183 0 : if (psResult == NULL)
184 0 : return NULL;
185 :
186 : /* -------------------------------------------------------------------- */
187 : /* One common mistake is to try and reuse the auth token. */
188 : /* After the first use it will return invalid_grant. */
189 : /* -------------------------------------------------------------------- */
190 0 : if( psResult->pabyData != NULL
191 : && strstr((const char *) psResult->pabyData,"invalid_grant") != NULL)
192 : {
193 0 : CPLString osURL;
194 0 : osURL.Seize( GOA2GetAuthorizationURL(pszScope) );
195 : CPLError( CE_Failure, CPLE_AppDefined,
196 : "Attempt to use a OAuth2 authorization code multiple times.\n"
197 : "Request a fresh authorization token at\n%s.",
198 0 : osURL.c_str() );
199 0 : CPLHTTPDestroyResult(psResult);
200 0 : return NULL;
201 : }
202 :
203 0 : if (psResult->pabyData == NULL ||
204 : psResult->pszErrBuf != NULL)
205 : {
206 0 : if( psResult->pszErrBuf != NULL )
207 0 : CPLDebug( "GOA2", "%s", psResult->pszErrBuf );
208 0 : if( psResult->pabyData != NULL )
209 0 : CPLDebug( "GOA2", "%s", psResult->pabyData );
210 :
211 : CPLError( CE_Failure, CPLE_AppDefined,
212 0 : "Fetching OAuth2 access code from auth code failed.");
213 0 : CPLHTTPDestroyResult(psResult);
214 0 : return NULL;
215 : }
216 :
217 : CPLDebug( "GOA2", "Access Token Response:\n%s",
218 0 : (const char *) psResult->pabyData );
219 :
220 : /* -------------------------------------------------------------------- */
221 : /* This response is in JSON and will look something like: */
222 : /* -------------------------------------------------------------------- */
223 : /*
224 : {
225 : "access_token" : "ya29.AHES6ZToqkIJkat5rIqMixR1b8PlWBACNO8OYbqqV-YF1Q13E2Kzjw",
226 : "token_type" : "Bearer",
227 : "expires_in" : 3600,
228 : "refresh_token" : "1/eF88pciwq9Tp_rHEhuiIv9AS44Ufe4GOymGawTVPGYo"
229 : }
230 : */
231 : CPLStringList oResponse = ParseSimpleJson(
232 0 : (const char *) psResult->pabyData );
233 0 : CPLHTTPDestroyResult(psResult);
234 :
235 0 : CPLString osAccessToken = oResponse.FetchNameValueDef( "access_token", "" );
236 0 : CPLString osRefreshToken = oResponse.FetchNameValueDef( "refresh_token", "" );
237 0 : CPLDebug("GOA2", "Access Token : '%s'", osAccessToken.c_str());
238 0 : CPLDebug("GOA2", "Refresh Token : '%s'", osRefreshToken.c_str());
239 :
240 0 : if( osRefreshToken.size() == 0)
241 : {
242 : CPLError( CE_Failure, CPLE_AppDefined,
243 0 : "Unable to identify a refresh token in the OAuth2 response.");
244 0 : return NULL;
245 : }
246 : else
247 : {
248 : // Currently we discard the access token and just return the refresh token
249 0 : return CPLStrdup(osRefreshToken);
250 0 : }
251 : }
252 :
253 : /************************************************************************/
254 : /* GOA2GetAccessToken() */
255 : /************************************************************************/
256 :
257 : /**
258 : * Fetch access token using refresh token.
259 : *
260 : * The permanent refresh token is used to fetch a temporary (usually one
261 : * hour) access token using Google OAuth2 web services.
262 : *
263 : * A CPLError will be reported if the request fails for some reason.
264 : * Common reasons include the refresh token having been revoked by the
265 : * user or http connection problems.
266 : *
267 : * @param pszRefreshToken the refresh token from GOA2GetRefreshToken().
268 : * @param pszScope the scope for which it is valid.
269 : *
270 : * @return access token, to be freed with CPLFree(), null on failure.
271 : */
272 :
273 7 : char *GOA2GetAccessToken( const char *pszRefreshToken,
274 : const char *pszScope )
275 : {
276 : /* -------------------------------------------------------------------- */
277 : /* Prepare request. */
278 : /* -------------------------------------------------------------------- */
279 7 : CPLString osItem;
280 7 : CPLStringList oOptions;
281 :
282 : oOptions.AddString(
283 7 : "HEADERS=Content-Type: application/x-www-form-urlencoded" );
284 :
285 : osItem.Printf(
286 : "POSTFIELDS="
287 : "refresh_token=%s"
288 : "&client_id=%s"
289 : "&client_secret=%s"
290 : "&grant_type=refresh_token",
291 : pszRefreshToken,
292 : CPLGetConfigOption("GOA2_CLIENT_ID", GDAL_CLIENT_ID),
293 7 : CPLGetConfigOption("GOA2_CLIENT_SECRET", GDAL_CLIENT_SECRET));
294 7 : oOptions.AddString(osItem);
295 :
296 : /* -------------------------------------------------------------------- */
297 : /* Submit request by HTTP. */
298 : /* -------------------------------------------------------------------- */
299 7 : CPLHTTPResult *psResult = CPLHTTPFetch(GOOGLE_AUTH_URL "/token", oOptions);
300 :
301 7 : if (psResult == NULL)
302 0 : return NULL;
303 :
304 7 : if (psResult->pabyData == NULL ||
305 : psResult->pszErrBuf != NULL)
306 : {
307 0 : if( psResult->pszErrBuf != NULL )
308 0 : CPLDebug( "GFT", "%s", psResult->pszErrBuf );
309 0 : if( psResult->pabyData != NULL )
310 0 : CPLDebug( "GFT", "%s", psResult->pabyData );
311 :
312 : CPLError( CE_Failure, CPLE_AppDefined,
313 0 : "Fetching OAuth2 access code from auth code failed.");
314 0 : CPLHTTPDestroyResult(psResult);
315 0 : return NULL;
316 : }
317 :
318 : CPLDebug( "GOA2", "Refresh Token Response:\n%s",
319 7 : (const char *) psResult->pabyData );
320 :
321 : /* -------------------------------------------------------------------- */
322 : /* This response is in JSON and will look something like: */
323 : /* -------------------------------------------------------------------- */
324 : /*
325 : {
326 : "access_token":"1/fFBGRNJru1FQd44AzqT3Zg",
327 : "expires_in":3920,
328 : "token_type":"Bearer"
329 : }
330 : */
331 : CPLStringList oResponse = ParseSimpleJson(
332 7 : (const char *) psResult->pabyData );
333 7 : CPLHTTPDestroyResult(psResult);
334 :
335 7 : CPLString osAccessToken = oResponse.FetchNameValueDef( "access_token", "" );
336 :
337 7 : CPLDebug("GOA2", "Access Token : '%s'", osAccessToken.c_str());
338 :
339 7 : if (osAccessToken.size() == 0)
340 : {
341 : CPLError( CE_Failure, CPLE_AppDefined,
342 0 : "Unable to identify an access token in the OAuth2 response.");
343 0 : return NULL;
344 : }
345 : else
346 7 : return CPLStrdup(osAccessToken);
347 : }
348 :
349 :
350 :
|