oauth2Introspection function
Middleware
oauth2Introspection(
- OAuthIntrospectionOptions options, {
- OAuthOnValidated? onValidated,
- Client? httpClient,
Creates a middleware for OAuth2 token introspection.
This middleware validates incoming OAuth2 tokens using the provided
options. If the token is valid, its claims and attributes are added
to the request context.
options: Configuration options for the introspection.onValidated: Optional callback invoked after successful validation.httpClient: Optional HTTP client for making introspection requests.
Returns a middleware function that can be used in the routing pipeline.
Example:
final middleware = oauth2Introspection(
OAuthIntrospectionOptions(
endpoint: Uri.parse('https://example.com/introspect'),
clientId: 'my-client-id',
clientSecret: 'my-client-secret',
),
);
Implementation
Middleware oauth2Introspection(
OAuthIntrospectionOptions options, {
OAuthOnValidated? onValidated,
http.Client? httpClient,
}) {
final client = httpClient ?? http.Client();
final cache = <String, _CachedIntrospection>{};
Future<OAuthIntrospectionResult> introspect(String token) async {
final cached = cache[token];
if (cached != null && !cached.isExpired) {
return cached.result;
}
final headers = <String, String>{
'Content-Type': 'application/x-www-form-urlencoded',
};
if (options.clientId != null && options.clientSecret != null) {
final credentials = base64Encode(
utf8.encode('${options.clientId}:${options.clientSecret}'),
);
headers['Authorization'] = 'Basic $credentials';
}
final body = <String, String>{
'token': token,
if (options.tokenTypeHint != null)
'token_type_hint': options.tokenTypeHint!,
...options.additionalParameters,
};
final response = await client.post(
options.endpoint,
headers: headers,
body: body,
);
if (response.statusCode < 200 || response.statusCode >= 300) {
throw OAuth2Exception(
'Introspection endpoint responded with ${response.statusCode}',
response.statusCode,
);
}
final Map<String, dynamic> jsonResponse =
json.decode(response.body) as Map<String, dynamic>;
final result = OAuthIntrospectionResult(
active: jsonResponse['active'] == true,
raw: jsonResponse,
);
cache[token] = _CachedIntrospection(
result,
DateTime.now().add(options.cacheTtl),
);
return result;
}
return (EngineContext ctx, Next next) async {
final header = ctx.request.header('Authorization');
if (header.isEmpty || !header.startsWith('Bearer ')) {
ctx.response
..statusCode = HttpStatus.unauthorized
..write('missing token');
return ctx.response;
}
final token = header.substring('Bearer '.length).trim();
if (token.isEmpty) {
ctx.response
..statusCode = HttpStatus.unauthorized
..write('missing token');
return ctx.response;
}
OAuthIntrospectionResult result;
try {
result = await introspect(token);
} on OAuth2Exception catch (error) {
ctx.response
..statusCode = HttpStatus.unauthorized
..write(error.message);
return ctx.response;
}
if (!result.active) {
ctx.response
..statusCode = HttpStatus.unauthorized
..write('token inactive');
return ctx.response;
}
final now = DateTime.now().toUtc();
final expiresAt = result.expiresAt?.toUtc();
if (expiresAt != null && expiresAt.add(options.clockSkew).isBefore(now)) {
ctx.response
..statusCode = HttpStatus.unauthorized
..write('token expired');
return ctx.response;
}
final notBefore = result.notBefore?.toUtc();
if (notBefore != null &&
notBefore.subtract(options.clockSkew).isAfter(now)) {
ctx.response
..statusCode = HttpStatus.unauthorized
..write('token not yet valid');
return ctx.response;
}
ctx.request
..setAttribute(oauthTokenAttribute, token)
..setAttribute(oauthClaimsAttribute, result.raw)
..setAttribute(oauthScopeAttribute, result.scope);
if (onValidated != null) {
await onValidated(result, ctx);
}
return await next();
};
}