verify method
Implementation
SeoActionFormContext? verify({
required String actionId,
required String token,
}) {
final action = actionId.trim();
if (!isValidSeoInteractionId(action)) return null;
if (token.length > seoActionFormMaxContextTokenLength) return null;
final separator = token.indexOf('.');
if (separator < 2 ||
separator != token.lastIndexOf('.') ||
token.length - separator - 1 != 43) {
return null;
}
final body = token.substring(0, separator);
final suppliedSignature = token.substring(separator + 1);
if (!_tokenPart.hasMatch(body) || !_tokenPart.hasMatch(suppliedSignature)) {
return null;
}
final List<int> payload;
try {
payload = base64Url.decode(_withPadding(body));
} on FormatException {
return null;
}
final expectedSignature = _signature(payload);
if (!_constantTimeEquals(expectedSignature, suppliedSignature)) {
return null;
}
final Object? decoded;
try {
decoded = jsonDecode(utf8.decode(payload, allowMalformed: false));
} on FormatException {
return null;
}
if (decoded is! Map<String, Object?> ||
decoded['action'] != action ||
decoded['values'] is! Map<String, Object?>) {
return null;
}
late final Map<String, Object> Function(Map<String, String>)
expectedEnvelope;
switch (decoded['schema']) {
case _seoActionFormContextUnboundedSchema:
if (tokenLifetime != null || decoded.length != 3) return null;
expectedEnvelope = (values) => _unboundedEnvelope(action, values);
break;
case _seoActionFormContextExpiringSchema:
final issuedAt = decoded['issuedAt'];
final expiresAt = decoded['expiresAt'];
final now = _epochMilliseconds(_clock());
if (decoded.length != 5 ||
issuedAt is! int ||
expiresAt is! int ||
!_validLifetime(issuedAt, expiresAt) ||
issuedAt > now ||
expiresAt <= now) {
return null;
}
final requiredLifetime = tokenLifetime;
if (requiredLifetime != null &&
expiresAt - issuedAt != requiredLifetime.inMilliseconds) {
return null;
}
expectedEnvelope = (values) => _expiringEnvelope(
action,
values,
issuedAt: issuedAt,
expiresAt: expiresAt,
);
break;
default:
return null;
}
final values = <String, String>{};
for (final entry in (decoded['values']! as Map<String, Object?>).entries) {
if (entry.value is! String) return null;
values[entry.key] = entry.value! as String;
}
final canonical = _canonicalContext(values);
if (canonical == null ||
_withoutPadding(base64Url.encode(
utf8.encode(jsonEncode(expectedEnvelope(canonical))),
)) !=
body) {
return null;
}
return SeoActionFormContext._(canonical);
}