Skip to content

Commit

Permalink
feat: add trySilentRefresh method to browser Authenticator
Browse files Browse the repository at this point in the history
  • Loading branch information
rbellens committed Dec 31, 2022
1 parent 65f9b28 commit e74d8e3
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 6 deletions.
2 changes: 2 additions & 0 deletions example/browser_example/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ <h1>openid_client Demo Web App</h1>
<div id="when-logged-in" style="display: none;">
<p>Hello <span id="name"></span>!</p>
<p>Your email is <span id="email"></span>.</p>
<p>Id token issued at: <span id="issuedAt"></span></p>
<button id="logout">logout</button>
<button id="refresh">refresh</button>
</div>
<div id="when-logged-out" style="display: none;">
<button id="login">login</button>
Expand Down
16 changes: 13 additions & 3 deletions example/browser_example/web/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,23 @@ Future<void> main() async {
var credential = await authenticator.credential;

if (credential != null) {
final userData = await credential.getUserInfo();
Future<void> refresh() async {
var userData = await credential!.getUserInfo();
document.querySelector('#name')!.text = userData.name!;
document.querySelector('#email')!.text = userData.email!;
document.querySelector('#issuedAt')!.text =
credential!.idToken.claims.issuedAt.toIso8601String();
}

await refresh();
document.querySelector('#when-logged-in')!.style.display = 'block';
document.querySelector('#name')!.text = userData.name!;
document.querySelector('#email')!.text = userData.email!;
document.querySelector('#logout')!.onClick.listen((_) async {
authenticator.logout();
});
document.querySelector('#refresh')!.onClick.listen((_) async {
credential = await authenticator.trySilentRefresh();
await refresh();
});
} else {
document.querySelector('#when-logged-out')!.style.display = 'block';
document.querySelector('#login')!.onClick.listen((_) async {
Expand Down
60 changes: 58 additions & 2 deletions lib/openid_client_browser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,72 @@ class Authenticator {
}

static Future<Credential?> _credentialFromUri(Flow flow) async {
var uri = Uri(query: Uri.parse(window.location.href).fragment);
var uri = Uri.parse(window.location.href);
var iframe = uri.queryParameters['iframe'] != null;
uri = Uri(query: uri.fragment);
var q = uri.queryParameters;
if (q.containsKey('access_token') ||
q.containsKey('code') ||
q.containsKey('id_token')) {
window.history.replaceState(
'', '', Uri.parse(window.location.href).removeFragment().toString());
window.localStorage.remove('openid_client:state');
return await flow.callback(q.cast());

var c = await flow.callback(q.cast());
if (iframe) window.parent!.postMessage(c.response, '*');
return c;
}
return null;
}

/// Tries to refresh the access token silently in a hidden iframe.
///
/// The implicit flow does not support refresh tokens. This method uses a
/// hidden iframe to try to get a new access token without the user having to
/// sign in again. It returns a [Future] that completes with a [Credential]
/// when the iframe receives a response from the authorization server. The
/// future will timeout after [timeout] if the iframe does not receive a
/// response.
Future<Credential> trySilentRefresh(
{Duration timeout = const Duration(seconds: 20)}) async {
var iframe = IFrameElement();
var url = flow.authenticationUri;
window.localStorage['openid_client:state'] = flow.state;
iframe.src = url.replace(queryParameters: {
...url.queryParameters,
'prompt': 'none',
'redirect_uri': flow.redirectUri.replace(queryParameters: {
...flow.redirectUri.queryParameters,
'iframe': 'true',
}).toString(),
}).toString();
iframe.style.display = 'none';
document.body!.append(iframe);
var event = await window.onMessage.first.timeout(timeout).whenComplete(() {
iframe.remove();
});
if (event.data is Map) {
var current = await credential;
if (current == null) {
return flow.client.createCredential(
accessToken: event.data['access_token'],
expiresAt: event.data['expires_at'] == null
? null
: DateTime.fromMillisecondsSinceEpoch(
int.parse(event.data['expires_at'].toString()) * 1000),
refreshToken: event.data['refresh_token'],
expiresIn: event.data['expires_in'] == null
? null
: Duration(
seconds: int.parse(event.data['expires_in'].toString())),
tokenType: event.data['token_type'],
idToken: event.data['id_token'],
);
} else {
return current..updateToken((event.data as Map).cast());
}
} else {
throw Exception('${event.data}');
}
}
}
12 changes: 11 additions & 1 deletion lib/src/openid.dart
Original file line number Diff line number Diff line change
Expand Up @@ -295,10 +295,20 @@ class Credential {
},
client: client.httpClient);

updateToken(json);
return _token;
}

/// Updates the token with the given [json] and notifies all listeners
/// of the new token.
///
/// This method is used internally by [getTokenResponse], but can also be
/// used to update the token manually, e.g. when no refresh token is available
/// and the token is updated by other means.
void updateToken(Map<String, dynamic> json) {
_token =
TokenResponse.fromJson({'refresh_token': _token.refreshToken, ...json});
_onTokenChanged.add(_token);
return _token;
}

Credential.fromJson(Map<String, dynamic> json, {http.Client? httpClient})
Expand Down

0 comments on commit e74d8e3

Please sign in to comment.