verify method

SeoActionFormContext? verify({
  1. required String actionId,
  2. required String token,
})

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);
}