重新验证身份¶
概述¶
Authany 中的重新验证身份基于 OIDC ID token 实现。ID 令牌是一个 JWT。
您的敏感操作服务器端点必须要求提供 ID 令牌。收到 ID 令牌后,您必须验证其签名。如果签名有效,您就可以信任 ID 令牌中的声明。
ID 令牌中的 auth_time 声明表示终端用户上次进行身份验证的时间。您应该检查 auth_time 声明,确认终端用户是否是在足够近的时间内完成身份验证的。
ID 令牌中的 https://authgear.com/claims/user/can_reauthenticate 声明表示该终端用户是否可以重新验证身份。如果该声明的值为 false,您可以根据自己的业务需求,选择允许终端用户继续操作,或禁止终端用户执行敏感操作。下面的图示展示了具体流程。


SDK 集成¶
以下代码示例展示了 SDK 与 Authany 之间的交互。
const biometricOptions = {
ios: {
localizedReason: 'Use biometric to authenticate',
constraint: 'biometryCurrentSet' as const,
},
android: {
title: 'Biometric Authentication',
subtitle: 'Biometric authentication',
description: 'Use biometric to authenticate',
negativeButtonText: 'Cancel',
constraint: ['BIOMETRIC_STRONG' as const],
invalidatedByBiometricEnrollment: true,
},
};
async function onClickPerformSensitiveOperation() {
// Step 1: Refresh the ID token to ensure the claims are up-to-date.
await authgear.refreshIDToken();
// Step 2: Check if the end-user can be reauthenticated.
const canReauthenticate = authgear.canReauthenticate();
if (!canReauthenticate) {
// Step 2.1: Depending on your business need, you may want to allow
// the end-user to proceed.
// Here we assume you want to proceed.
const idTokenHint = authgear.getIDTokenHint();
// Step 2.2: Call the sensitive endpoint with the ID token.
// It is still required to pass the ID token to the endpoint so that
// the endpoint can know the end-user CANNOT be reauthenticated.
return callMySensitiveEndpoint(idTokenHint);
}
// Step 3: The end-user can be reauthenticated.
// If your app supports biometric authentication, you can pass
// the biometric options to reauthenticate.
// If biometric is enabled for the current user, it will be used instead.
await authgear.reauthenticate({
redirectURI: THE_REDIRECT_URI,
}, biometricOptions);
// Step 4: If we reach here, the reauthentication was done.
// The ID token have up-to-date auth_time claim.
const idTokenHint = authgear.getIDTokenHint();
return callMySensitiveEndpoint(idTokenHint);
}
final ios = BiometricOptionsIOS(
localizedReason: "Use biometric to authenticate",
constraint: BiometricAccessConstraintIOS.biometryAny,
);
final android = BiometricOptionsAndroid(
title: "Biometric Authentication",
subtitle: "Biometric authentication",
description: "Use biometric to authenticate",
negativeButtonText: "Cancel",
constraint: [BiometricAccessConstraintAndroid.biometricStrong],
invalidatedByBiometricEnrollment: false,
);
Future<void> onClickPerformSensitiveOperation() async {
// Step 1: Refresh the ID token to ensure the claims are up-to-date.
await authgear.refreshIDToken();
// Step 2: Check if the end-user can be reauthenticated.
final canReauthenticate = authgear.canReauthenticate;
if (!canReauthenticate) {
// Step 2.1: Depending on your business need, you may want to allow
// the end-user to proceed.
// Here we assume you want to proceed.
final idTokenHint = authgear.idTokenHint;
// Step 2.2: Call the sensitive endpoint with the ID token.
// It is still required to pass the ID token to the endpoint so that
// the endpoint can know the end-user CANNOT be reauthenticated.
return callMySensitiveEndpoint(idTokenHint);
}
// Step 3: The end-user can be reauthenticated.
// If your app supports biometric authentication, you can pass
// the biometric options to reauthenticate.
// If biometric is enabled for the current user, it will be used instead.
await authgear.reauthenticate(
redirectURI: THE_REDIRECT_URI,
biometricIOS: ios,
biometricAndroid: android,
);
// Step 4: If we reach here, the reauthentication was done.
// The ID token have up-to-date auth_time claim.
final idTokenHint = authgear.idTokenHint;
return callMySensitiveEndpoint(idTokenHint);
}
var ios = new BiometricOptionsIos
{
LocalizedReason = "Use biometric to authenticate",
AccessConstraint = BiometricAccessConstraintIos.BiometricAny,
};
var android = new BiometricOptionsAndroid
{
Title = "Biometric Authentication",
Subtitle = "Biometric authentication",
Description = "Use biometric to authenticate",
NegativeButtonText = "Cancel",
AccessConstraint = BiometricAccessConstraintAndroid.BiometricOnly,
InvalidatedByBiometricEnrollment = false,
};
async void OnPerformSensitiveOperationClicked(object sender, EventArgs args)
{
// Step 1: Refresh the ID token to ensure the claims are up-to-date.
await authgear.RefreshIdTokenAsync();
// Step 2: Check if the end-user can be reauthenticated.
var canReauthenticate = authgear.CanReauthenticate;
if (!canReauthenticate)
{
// Step 2.1: Depending on your business need, you may want to allow
// the end-user to proceed.
// Here we assume you want to proceed.
var idTokenHint = authgear.IdTokenHint;
// Step 2.2: Call the sensitive endpoint with the ID token.
// It is still required to pass the ID token to the endpoint so that
// the endpoint can know the end-user CANNOT be reauthenticated.
await CallMySensitiveEndpointAsync(idTokenHint);
return;
}
// Step 3: The end-user can be reauthenticated.
// If your app supports biometric authentication, you can pass
// the biometric options to reauthenticate.
// If biometric is enabled for the current user, it will be used instead.
await authgear.ReauthenticateAsync(new ReauthenticateOptions
{
RedirectURI: THE_REDIRECT_URI,
}, new BiometricOptions
{
Ios = ios,
Android = android,
});
// Step 4: If we reach here, the reauthentication was done.
// The ID token have up-to-date auth_time claim.
var idTokenHint = authgear.IdTokenHint;
await CallMySensitiveEndpointAsync(idTokenHint);
}
async function onClickPerformSensitiveOperation() {
// Step 1: Refresh the ID token to ensure the claims are up-to-date.
await authgear.refreshIDToken();
// Step 2: Check if the end-user can be reauthenticated.
const canReauthenticate = authgear.canReauthenticate();
if (!canReauthenticate) {
// Step 2.1: Depending on your business need, you may want to allow
// the end-user to proceed.
// Here we assume you want to proceed.
const idTokenHint = authgear.getIDTokenHint();
// Step 2.2: Call the sensitive endpoint with the ID token.
// It is still required to pass the ID token to the endpoint so that
// the endpoint can know the end-user CANNOT be reauthenticated.
return callMySensitiveEndpoint(idTokenHint);
}
// Step 3: The end-user can be reauthenticated.
// The end-user will be redirected to Authgear.
// When the reauthentication finishes,
// The end-user will be redirected back to the given redirect URI.
await authgear.startReauthentication({
redirectURI: THE_REDIRECT_URI
});
}
// Suppose the following function is run when the end-user is redirected to
// the redirect URI
async function onRedirectAfterReauthentication() {
// You HAVE to configure authgear again
// because your website have been visited freshly.
await authgear.finishReauthentication();
await authgear.refreshIDToken();
const idTokenHint = authgear.getIDTokenHint();
return callMySensitiveEndpoint(idTokenHint);
}
func onClickPerformSensitiveOperation() {
// Step 1: Refresh the ID token to ensure the claims are up-to-date.
authgear.refreshIDToken() { result in
switch result {
case .success:
// Step 2: Check if the end-user can be reauthenticated.
let canReauthenticate = authgear.canReauthenticate
if !canReauthenticate {
// Step 2.1: Depending on your business need, you may want to allow
// the end-user to proceed.
// Here we assume you want to proceed.
let idTokenHint = authgear.idTokenHint
// Step 2.2: Call the sensitive endpoint with the ID token.
// It is still required to pass the ID token to the endpoint
// so that the endpoint can know the end-user CANNOT
// be reauthenticated.
callMySensitiveEndpoint(idTokenHint)
return
}
// Step 3: The end-user can be reauthenticated.
// By default biometric is used for reauthentication if it is enabled for the current user.
// If you do not want biometric to be used, specify skipUsingBiometric: true
authgear.reauthenticate(redirectURI: THE_REDIRECT_URI, skipUsingBiometric: false) { result in
switch result {
case .success:
// Step 4: If we reach here, the reauthentication was done.
// The ID token have up-to-date auth_time claim.
let idTokenHint = authgear.idTokenHint
callMySensitiveEndpoint(idTokenHint)
return
case let .failure(error):
// Handle the error
}
}
case let .failure(error):
// Handle the error
}
}
}
public void onClickPerformSensitiveOperation() {
BiometricOptions biometricOptions = new BiometricOptions(
activity, // FragmentActivity
"Biometric authentication", // title
"Biometric authentication", // subtitle
"Use biometric to authenticate", // description
"Cancel", // negativeButtonText
ALLOWED, // allowedAuthenticators
true // invalidatedByBiometricEnrollment
);
// Step 1: Refresh the ID token to ensure the claims are up-to-date.
authgear.refreshIDToken(new OnRefreshIDTokenListener() {
@Override
public void onFailed(Throwable throwable) {
// Handle error
}
@Override
public void onFinished() {
// Step 2: Check if the end-user can be reauthenticated.
boolean canReauthenticate = authgear.getCanReauthenticate();
if (!canReauthenticate) {
// Step 2.1: Depending on your business need, you may want to allow
// the end-user to proceed.
// Here we assume you want to proceed.
String idTokenHint = authgear.getIDTokenHint();
// Step 2.2: Call the sensitive endpoint with the ID token.
// It is still required to pass the ID token to the endpoint
// so that the endpoint can know the end-user CANNOT
// be reauthenticated.
callMySensitiveEndpoint(idTokenHint);
return;
}
// Step 3: The end-user can be reauthenticated.
// If your app supports biometric authentication, you can pass
// the biometric options to reauthenticate.
// If biometric is enabled for the current user, it will be used.
ReauthenticateOptions options =
new ReauthenticateOptions(THE_REDIRECT_URI);
authgear.reauthenticate(options, biometricOptions, new OnReauthenticateListener() {
@Override
public void onFailed(Throwable throwable) {
// Handle error
}
@Override
public void onFinished(UserInfo userInfo) {
// Step 4: If we reach here, the reauthentication was done.
// The ID token have up-to-date auth_time claim.
String idTokenHint = authgear.getIDTokenHint();
callMySensitiveEndpoint(idTokenHint);
return;
}
});
}
});
}
根据上次身份验证时间条件性地重新验证身份¶
如果您应用中的终端用户经常连续执行一系列敏感操作,每次操作前都要求其重新验证身份会带来不便。为了让最近已经完成重新验证身份的终端用户可以跳过这一步骤,SDK 允许您查看终端用户的上次身份验证时间。
async function onClickPerformSensitiveOperation() {
await authgear.refreshIDToken();
// Before you trigger reauthentication, check authTime first.
const authTime = authgear.getAuthTime();
if (authTime != null) {
const now = new Date();
const timeDelta = now.getTime() - authTime.getTime();
if (timeDelta < 5 * 60 * 1000 /* 5 minutes */) {
const idTokenHint = authgear.getIDTokenHint();
return callMySensitiveEndpoint(idTokenHint);
}
}
// Otherwise trigger authentication.
}
func onClickPerformSensitiveOperation() {
authgear.refreshIDToken() { result in
switch result {
case .success:
// Before you trigger reauthentication, check authTime first.
if let authTime = authgear.authTime {
let now = Date()
let timeDelta = now.timeIntervalSince(authTime)
if timeDelta < 5 * 60 {
let idTokenHint = authgear.idTokenHint
callMySensitiveEndpoint(idTokenHint)
return
}
}
// Otherwise trigger authentication.
case let .failure(error):
// Handle the error
}
}
}
public void onClickPerformSensitiveOperation() {
authgear.refreshIDToken(new OnRefreshIDTokenListener() {
@Override
public void onFailed(Throwable throwable) {
// Handle error
}
@Override
public void onFinished() {
// Before you trigger reauthentication, check authTime first.
Date authTime = authgear.getAuthTime();
if (authTime != null) {
Date now = new Date();
long timedelta = now.getTime() - authTime.getTime();
if (timedelta < 5 * 60 * 1000) {
String idTokenHint = authgear.getIDTokenHint();
callMySensitiveEndpoint(idTokenHint);
return;
}
}
// Otherwise trigger authentication.
}
});
}
public async void OnPerformSensitiveOperationClicked(object sender, EventArgs args)
{
await authgear.RefreshIdTokenAsync();
var authTime = authgear.AuthTime;
if (authTime != null)
{
var now = DateTimeOffset.UtcNow;
var timedelta = now - authTime.Value;
if (timedelta < TimeSpan.FromMinutes(5))
{
var idTokenHint = authgear.IdTokenHint;
callMySensitiveEndpoint(idTokenHint);
return;
}
}
}
后端集成¶
最后,您需要在后端验证 ID 令牌的签名,然后校验其中的声明。
import json
from contextlib import closing
from urllib.request import urlopen
from datetime import datetime, timezone, timedelta
import jwt
from jwt import PyJWKClient
base_address = "https://<your_app_endpoint>"
def fetch_jwks_uri(base_address):
doc_url = base_address + "/.well-known/openid-configuration"
with closing(urlopen(doc_url)) as f:
doc = json.load(f)
jwks_uri = doc["jwks_uri"]
if not jwks_uri:
raise Exception('Failed to fetch jwks uri.')
return jwks_uri
def my_endpoint():
id_token = GET_ID_TOKEN_FROM_HTTP_REQUEST_SOMEHOW()
try:
jwks_uri = fetch_jwks_uri(base_address)
# Reuse PyJWKClient for better performance
jwks_client = PyJWKClient(jwks_uri)
signing_key = jwks_client.get_signing_key_from_jwt(id_token)
claims = jwt.decode(
id_token,
signing_key.key,
algorithms=["RS256"],
audience=base_address,
options={"verify_exp": True},
)
auth_time = claims["auth_time"]
dt = datetime.fromtimestamp(auth_time)
now = datetime.utcnow()
delta = now - dt
if delta > timedelta(minutes=5):
raise ValueError("auth_time is not recent enough")
except:
# Handle error
raise
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/lestrrat-go/jwx/v3/jwk"
"github.com/lestrrat-go/jwx/v3/jwt"
)
var (
baseAddress = "https://<your_app_endpoint>"
)
type OIDCDiscoveryDocument struct {
JWKSURI string `json:"jwks_uri"`
}
func FetchOIDCDiscoveryDocument(endpoint string) (*OIDCDiscoveryDocument, error) {
resp, err := http.DefaultClient.Get(endpoint)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf(
"failed to fetch discovery document: unexpected status code: %d",
resp.StatusCode,
)
}
var document OIDCDiscoveryDocument
err = json.NewDecoder(resp.Body).Decode(&document)
if err != nil {
return nil, err
}
return &document, nil
}
func FetchJWK(baseAddress string) (jwk.Set, error) {
doc, err := FetchOIDCDiscoveryDocument(
baseAddress + "/.well-known/openid-configuration",
)
if err != nil {
return nil, err
}
set, err := jwk.Fetch(context.Background(), doc.JWKSURI)
return set, err
}
func CheckIDToken(idToken string) error {
// fetch jwks_uri from Authgear
// you can cache the value of jwks to have better performance
set, err := FetchJWK(baseAddress)
if err != nil {
return fmt.Errorf("failed to fetch JWK: %s", err)
}
// parse jwt token
token, err := jwt.ParseString(
idToken,
// This may not work out of the box depending on the jwk.Set.
// Please read about requirements for "kid" and "alg" (and possibly
// "WithDefaultKey") when using jwk.Set in the jwt.WithKeySet documentation.
jwt.WithKeySet(set),
)
if err != nil {
return fmt.Errorf("invalid token: %s", err)
}
// validate jwt token
err = jwt.Validate(token,
jwt.WithClock(jwt.ClockFunc(
func() time.Time { return time.Now().UTC() },
)),
jwt.WithIssuer(baseAddress),
)
if err != nil {
return fmt.Errorf("invalid token: %s", err)
}
var authTimeUnix float64
if err := token.Get("auth_time", &authTimeUnix); err != nil {
return fmt.Errorf("no auth_time: %w", err)
}
authTime := time.Unix(int64(authTimeUnix), 0)
now := time.Now().UTC()
diff := now.Sub(authTime)
if diff > 5*time.Minute {
return fmt.Errorf("auth_time is not recent enough")
}
return nil
}