こんにちは、株式会社Pentagonでエンジニアをしている石渡港です。
目次
この記事の内容
-
PCKE(Proof Key for Code Exchange)認証とは
フローの詳細はこちら
PKCE(Proof Key for Code Exchange 通称ピクシー)とは認可コード横取り攻撃の対策として提案された仕様です。
コードとコードチャレンジ、コード検証を利用して横取りの対策をしています。
詳しくはこちら -
この記事どんな記事?
FlutterとAuth0を利用したPKCEフローOAuth認証を実装できるようになります -
なぜ書いているの?
詳しく書いている記事がなかったため
実装方法
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter_appauth/flutter_appauth.dart';
import 'package:flutter_flavor/flutter_flavor.dart';
import 'package:http/http.dart' as http;
class Auth0Service {
/// -----------------------------------
/// External Packages
/// -----------------------------------
final FlutterAppAuth appAuth = FlutterAppAuth();
/// -----------------------------------
/// Auth0 Variables
/// -----------------------------------
String get auth0Domain => 'your domain';
String get auth0ClientId =>
'your client id';
String get auth0RedirectUri => 'your redirect uri';
String get aut0Issuer => 'https://$auth0Domain';
/// IdToken parse -> user profile etc...
Map<String, dynamic> _parseIdToken(String idToken) {
final parts = idToken.split(r'.');
assert(parts.length == 3);
return jsonDecode(
utf8.decode(
base64Url.decode(
base64Url.normalize(
parts[1],
),
),
),
) as Map<String, dynamic>;
}
/// get Auth0 user details
Future<Map<String, dynamic>> _getUserDetails(String accessToken) async {
final url = 'https://$auth0Domain/userinfo';
final response = await http.get(
Uri.parse(url),
headers: {'Authorization': '$Token Type $accessToken'},
);
if (response.statusCode == 200) {
return jsonDecode(response.body) as Map<String, dynamic>;
} else {
throw Exception('Failed to get user details');
}
}
/// Auth0 login
Future<AuthorizationTokenResponse?> loginAction() async {
final result = await login();
return result;
}
/// Auth0 get code
Future<AuthorizationTokenResponse?> login() async {
try {
final serviceConfiguration = AuthorizationServiceConfiguration(
'https://$auth0Domain/authorize', 'https://$auth0Domain/oauth/token');
final result = await appAuth.authorizeAndExchangeCode(
AuthorizationTokenRequest(
auth0ClientId, auth0RedirectUri,
issuer: 'https://$auth0Domain',
serviceConfiguration: serviceConfiguration,
scopes: [
'openid',
'profile',
'email',
'offline_access',
'api',
],
additionalParameters: {
'audience': 'your audience',
},
promptValues: [
'login'
], // ignore any existing session; force interactive login prompt
),
);
/// SecureはSecureStoregeをWrapしたクラス
if (result.tokenType != null) {
await Secure.setTokenType(result.tokenType);
}
if (result.accessTokenExpirationDateTime != null) {
await Secure.setAccessTokenExpire(
result.accessTokenExpirationDateTime?.toLongDateString());
}
if (result.accessToken != null) {
await Secure.setAccessToken(result.accessToken);
}
if (result.refreshToken != null) {
await Secure.setRefreshToken(result.refreshToken);
}
if (result.idToken != null) {
await Secure.setIdToken(result.idToken);
}
return result;
} on Object catch (e) {
final exception = e as PlatformException;
if (exception.code != 'authorize_and_exchange_code_failed') {
rethrow;
}
}
}
/// リフレッシュするか否か
Future<bool> doRefresh() async {
final _isLogin = await isLogin();
if (!_isLogin) {
return false;
}
final _isExpireAccessToken = await isExpireAccessToken();
return _isExpireAccessToken;
}
/// ログイン中か否か
Future<bool> isLogin() async {
final storedRefreshToken = await getStoredRefreshToken();
if (storedRefreshToken?.isEmpty ?? true) {
return false;
}
return true;
}
/// アクセストークンが期限切れか否か
Future<bool> isExpireAccessToken() async {
final expireDateTimeString = await Secure.accessTokenExpire;
if (expireDateTimeString?.isEmpty ?? true) {
return true;
}
final expireDate = DateTime.parse(expireDateTimeString!);
final toDay = DateTime.now();
final isAfter = toDay.isAfter(expireDate);
return isAfter;
}
/// リフレッシュトークンの取得
Future<String?> getStoredRefreshToken() async {
return Secure.refreshToken;
}
/// リフレッシュトークンを利用して更新
Future<void> refreshToken() async {
try {
final storedRefreshToken = await getStoredRefreshToken();
final response = await appAuth.token(TokenRequest(
auth0ClientId,
auth0RedirectUri,
issuer: aut0Issuer,
refreshToken: storedRefreshToken,
grantType: GrantType.refreshToken,
scopes: [
'openid',
'profile',
'email',
'offline_access',
'api',
],
));
final idToken = _parseIdToken(response!.idToken!);
print(idToken.toString());
final profile = await _getUserDetails(response.accessToken!);
print(profile.toString());
await Secure.setAccessToken(response.accessToken);
if (response.refreshToken != null) {
await Secure.setRefreshToken(response.refreshToken);
}
} on Object catch (e, s) {
print('error on refresh token: $e - stack: $s');
await logoutAction();
}
}
/// ログアウト
Future<void> logoutAction() async {
await Secure.deleteAll();
}
}
注意点
FlutterAppAuth#authorizeAndExchangeCode
でコード交換まで対応してくれるadditionalParameters
を指定すること、指定しないと通常のフロー扱いとなり、access_token
などが想定外のものが返ってくる- scopesに
offline_access
を指定、なおかつAuth0でAllow Offline Access設定を有効にしないとrefresh_token
が帰ってこないこと