-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Automatic management of tokens for API in general #34
Open
jdubpark
wants to merge
7
commits into
feed-test
Choose a base branch
from
feed-api
base: feed-test
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
b370bdc
Automate token management
jdubpark eb2359d
Add api version to base url
jdubpark 62237ec
Fix token saving error
jdubpark 9237a61
Move cookie async to method
jdubpark 13c6559
Fix Dio interceptor returns
jdubpark 82be369
Cut some errors
jdubpark e13e891
Cut more errors
jdubpark File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" | ||
#include "Generated.xcconfig" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" | ||
#include "Generated.xcconfig" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
/* | ||
* Our own implementation of Dio cookie manger | ||
* Modified from: https://github.com/flutterchina/dio/blob/master/plugins/cookie_manager/lib/src/cookie_mgr.dart | ||
* | ||
* What changed? We store cookie in baseUrl instead of full Uri to allow using | ||
* cookie across the app (with requests many different routes) | ||
*/ | ||
|
||
import 'dart:async'; | ||
import 'dart:io'; | ||
|
||
import 'package:cookie_jar/cookie_jar.dart'; | ||
import 'package:dio/dio.dart'; | ||
|
||
/// Don't use this class in Browser environment | ||
class CookieManager extends Interceptor { | ||
/// Cookie manager for http requests。Learn more details about | ||
/// CookieJar please refer to [cookie_jar](https://github.com/flutterchina/cookie_jar) | ||
final CookieJar cookieJar; | ||
|
||
CookieManager(this.cookieJar); | ||
|
||
@override | ||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) { | ||
cookieJar.loadForRequest(options.uri).then((cookies) { | ||
var cookie = getCookies(cookies); | ||
if (cookie.isNotEmpty) { | ||
options.headers[HttpHeaders.cookieHeader] = cookie; | ||
} | ||
handler.next(options); | ||
}).catchError((e, stackTrace) { | ||
var err = DioError(requestOptions: options, error: e); | ||
err.stackTrace = stackTrace; | ||
handler.reject(err, true); | ||
}); | ||
} | ||
|
||
@override | ||
void onResponse(Response response, ResponseInterceptorHandler handler) { | ||
_saveCookies(response) | ||
.then((_) => handler.next(response)) | ||
.catchError((e, stackTrace) { | ||
var err = DioError(requestOptions: response.requestOptions, error: e); | ||
err.stackTrace = stackTrace; | ||
handler.reject(err, true); | ||
}); | ||
} | ||
|
||
@override | ||
void onError(DioError err, ErrorInterceptorHandler handler) { | ||
if (err.response != null) { | ||
_saveCookies(err.response!) | ||
.then((_) => handler.next(err)) | ||
.catchError((e, stackTrace) { | ||
var _err = DioError( | ||
requestOptions: err.response!.requestOptions, | ||
error: e, | ||
); | ||
_err.stackTrace = stackTrace; | ||
handler.next(_err); | ||
}); | ||
} else { | ||
handler.next(err); | ||
} | ||
} | ||
|
||
Future<void> _saveCookies(Response response) async { | ||
var cookies = response.headers[HttpHeaders.setCookieHeader]; | ||
|
||
if (cookies != null) { | ||
await cookieJar.saveFromResponse( | ||
response.requestOptions.uri, | ||
cookies.map((str) => Cookie.fromSetCookieValue(str)).toList(), | ||
); | ||
} | ||
} | ||
|
||
static String getCookies(List<Cookie> cookies) { | ||
return cookies.map((cookie) => '${cookie.name}=${cookie.value}').join('; '); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,45 +1,121 @@ | ||
import 'dart:convert'; | ||
import 'dart:developer'; | ||
import 'dart:io'; | ||
|
||
import 'package:http/http.dart'; | ||
import 'package:http/io_client.dart'; | ||
import 'package:cookie_jar/cookie_jar.dart'; | ||
import 'package:dio/dio.dart'; | ||
//import 'package:dio_cookie_manager/dio_cookie_manager.dart'; | ||
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; | ||
import 'package:oneplace_illinois/src/misc/config.dart'; | ||
import 'package:oneplace_illinois/src/misc/exceptions.dart'; | ||
import 'package:oneplace_illinois/src/misc/cookieManager.dart'; | ||
import 'package:oneplace_illinois/src/services/firebaseAuth.dart'; | ||
|
||
class ApiService extends IOClient { | ||
static final _loginUri = Uri.http(Config.baseEndpoint!, '/api/v1/user/login'); | ||
static const _refreshCookieName = 'refresh_jwt'; | ||
class ApiService { | ||
// NOTE: Signed cookie doesn't work with HTTP request. Only works with HTTPS. | ||
static final _baseUrl = Uri.https(Config.baseEndpoint!, ''); | ||
|
||
static PersistCookieJar _cookieJar = new PersistCookieJar(ignoreExpires: false); | ||
/* // use default settings for now | ||
static Future<PersistCookieJar> get cookieJar async { | ||
if (_cookieJar == null) { | ||
Directory appDocDir = await getApplicationDocumentsDirectory(); | ||
String appDocPath = appDocDir.path; | ||
_cookieJar = new PersistCookieJar({ | ||
ignoreExpires: false, // don't save/load expired cookies | ||
storage: appDocPath + '/.cookies/' | ||
}); | ||
} | ||
return _cookieJar; | ||
} | ||
*/ | ||
|
||
final storage = new FlutterSecureStorage(); // for storing access token | ||
|
||
FirebaseAuthService _firebaseAuth; | ||
String? _refreshToken; | ||
String? _accessToken; | ||
|
||
ApiService({required firebaseAuth}) : this._firebaseAuth = firebaseAuth; | ||
|
||
@override | ||
Future<IOStreamedResponse> send(BaseRequest request) async { | ||
if (_refreshToken == null || _accessToken == null) { | ||
|
||
Future<Dio> getClient() async { | ||
final Dio _dio = Dio(); | ||
|
||
_dio.options.baseUrl = '$_baseUrl/api/v1'; | ||
_dio.interceptors.clear(); | ||
|
||
_dio.interceptors.add(InterceptorsWrapper( | ||
onResponse: (Response response, handler) async { | ||
var data = response.data; | ||
|
||
if (data['error'] != null) { | ||
log(data.toString()); | ||
throw ApiException(data['error']); | ||
} | ||
|
||
// save access token (refresh token is auto-saved by CookieManager above) | ||
if (data['payload']?['accessToken'] != null && data['payload']?['accessToken'] != '') { | ||
await storage.write(key: 'jwt_access', value: data['payload']?['accessToken']); | ||
} | ||
|
||
return data; | ||
}, | ||
)); | ||
|
||
return _dio; | ||
} | ||
|
||
Future<Dio> _getClientWithAuth() async { | ||
// retrieve access token | ||
String? _accessToken = await storage.read(key: 'jwt_access'); | ||
|
||
if (_accessToken == null) { | ||
// interceptor from _getClient() used by _login() will auto-save | ||
// the new access & refresh token (for later requests) | ||
await _login(); | ||
} | ||
|
||
request.headers.addAll({ | ||
'Authorization': 'Bearer ${_accessToken!}', | ||
'Cookie': '$_refreshCookieName=${_refreshToken!}', | ||
}); | ||
final Dio _dio = Dio(); | ||
|
||
_dio.options.baseUrl = '$_baseUrl/api/v1'; | ||
_dio.interceptors.clear(); | ||
|
||
// Add cookie (refresh token) to interceptor | ||
_dio.interceptors.add(CookieManager(_cookieJar)); | ||
|
||
_dio.interceptors.add(InterceptorsWrapper( | ||
onRequest: (RequestOptions options, handler) { | ||
// Add access token to interceptor | ||
options.headers['Authorization'] = 'Bearer ' + _accessToken!; | ||
handler.next(options); | ||
}, | ||
onResponse: (Response response, handler) async { | ||
var data = response.data; | ||
|
||
return super.send(request); | ||
if (data['error'] != null) { | ||
log(data.toString()); | ||
throw ApiException(data['error']); | ||
} | ||
|
||
// Save access token if new one is issued from the server | ||
if (data['payload'] && data['payload']['accessToken']) { | ||
await storage.write(key: 'jwt_access', value: data['payload']['accessToken']); | ||
} | ||
|
||
// return response; | ||
return data; | ||
} | ||
)); | ||
return _dio; | ||
} | ||
|
||
Future<void> _login() async { | ||
var user = await _firebaseAuth.userStream.first; | ||
Response response = await post(_loginUri, body: { | ||
|
||
Dio client = await getClient(); | ||
|
||
Response response = await client.post('/user/login', data: { | ||
'email': user!.email, | ||
'token': await user.getIdToken(), | ||
}); | ||
|
||
var cookies = response.headers['set-cookie']!; | ||
_refreshToken = cookies.split(';')[0].split('=')[1]; | ||
_accessToken = jsonDecode(response.body)['payload']['accessToken']; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think we should just go with getClientWithAuth alone, since it's probably good to make all our API requests authenticated even if not yet necessary (maybe validation is added later)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I think there's a way to extend the Dio client, which would make ApiService itself available for running gets, posts, etc. with the credentials automatically applied. See here: https://pub.dev/packages/dio#extends-dio-class
Not sure if this will do what we need it do though, as the docs aren't good in this section. It may allow for the removal of the
getClient
method though, which I think makes things clearer when the code is used.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we want to use just
getClientWithAuth
, then we need to modify it to allow request even if there is no access token (for/section/search
and/course/search
). Extending the Dio class would be nice, but I don't see the need for it at the moment. If you are able to do it without much hassle, sure go for it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we extend the class, we could pass an additional option (part of
options
) to distinguish between auth & no-auth requests.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah extending the Dio class may be as much of a hassle as extending IOClient is. Does running a
/section/search
with some query parameters and an access token cause an error?If it does, then I suppose we will need to have separate code paths for authenticated/unauthenticated requests. I propose changing the method
getClient
to use a keyword argumentcredentials
orauth
or something that can be true or false.