Skip to content

Commit 4fd9139

Browse files
loevgaardclaude
andcommitted
Replace Criipto OIDC integration with VerifyID REST API
Criipto used OpenID Connect (JWT decoding, JWKS keys, token exchange). VerifyID uses a simpler REST API with two endpoints: URL generation and age checking. Device ID is generated server-side (UUID v4) and stored in the session. Breaking changes: - Config: `criipto.*` replaced with `verify_id.plugin_key` - Env vars: CRIIPTO_* replaced with VERIFYID_PLUGIN_KEY - MinimumAge enum: removed cases 15 and 21 (VerifyID supports 16/18 only) - Removed firebase/php-jwt and cuyz/valinor dependencies Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e42d162 commit 4fd9139

28 files changed

+678
-316
lines changed

composer.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,8 @@
99
],
1010
"require": {
1111
"php": ">=8.1",
12-
"cuyz/valinor": "^1.13",
1312
"doctrine/orm": "^2.0 || ^3.0",
1413
"doctrine/persistence": "^3.4",
15-
"firebase/php-jwt": "^6.10",
1614
"setono/doctrine-orm-trait": "^1.1",
1715
"sylius/admin-bundle": "^1.0",
1816
"sylius/core": "^1.0",
@@ -23,10 +21,11 @@
2321
"symfony/dependency-injection": "^6.4 || ^7.0",
2422
"symfony/event-dispatcher": "^6.4 || ^7.0",
2523
"symfony/form": "^6.4 || ^7.0",
26-
"symfony/http-client": "^6.4 || ^7.0",
24+
"symfony/http-client-contracts": "^3.4",
2725
"symfony/http-foundation": "^6.4 || ^7.0",
2826
"symfony/http-kernel": "^6.4 || ^7.0",
2927
"symfony/routing": "^6.4 || ^7.0",
28+
"symfony/uid": "^6.4 || ^7.0",
3029
"twig/twig": "^2.0 || ^3.0",
3130
"webmozart/assert": "^1.11"
3231
},
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-03-06
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
## Context
2+
3+
The plugin currently uses Criipto as an OpenID Connect provider for age verification. The OIDC flow involves discovering endpoints via `.well-known/openid-configuration`, building authorization URLs with scopes like `is_over_18`, exchanging authorization codes for JWT id_tokens, and decoding those tokens with JWKS keys.
4+
5+
VerifyID offers a simpler REST API: generate a verification URL, redirect the user, receive a token back, and check the token against an endpoint. No OIDC, no JWTs, no key management.
6+
7+
The current codebase has these Criipto-specific components:
8+
- `OpenIdConfiguration` / `OpenIdConfigurationFactory` — fetches OIDC discovery document and JWKS keys
9+
- `TokenDecoder` / `DecodedToken` — decodes JWT id_tokens using firebase/php-jwt
10+
- `AuthorizationUrlGenerator` — builds OIDC authorization URLs with scopes
11+
- `CriiptoCallbackAction` — exchanges auth code for token, decodes it, stores result
12+
- `Configuration` — defines `criipto` config block with `client_id`, `client_secret`, `verify_domain`
13+
14+
## Goals / Non-Goals
15+
16+
**Goals:**
17+
- Replace Criipto with VerifyID as the age verification provider
18+
- Keep the entire flow server-side (no client-side JavaScript required)
19+
- Simplify the integration — fewer moving parts, fewer dependencies
20+
- Maintain the existing checkout UX pattern (button on checkout complete page)
21+
22+
**Non-Goals:**
23+
- Supporting multiple providers simultaneously
24+
- Supporting provider abstraction/interface for swapping providers
25+
- Backward compatibility with Criipto configuration
26+
- Supporting ages 15 or 21 (not supported by VerifyID)
27+
28+
## Decisions
29+
30+
### 1. Server-side device_id generation
31+
32+
**Decision**: Generate UUID v4 server-side and store in the Symfony session.
33+
34+
**Alternatives considered**:
35+
- Client-side JS via VerifyID's `device_id.js` script — would require AJAX or form submission to pass the device_id to the server, complicating the template
36+
- Passing device_id through URL parameters — exposes it unnecessarily
37+
38+
**Rationale**: The `device_id.js` script just generates a UUID v4 and stores it in localStorage. We can generate the same UUID server-side with `Uuid::v4()`. Storing it in the session keeps it available across the redirect flow (initiate → VerifyID → callback) without any client-side dependencies.
39+
40+
### 2. Two-phase redirect flow
41+
42+
**Decision**: The "Verify Age" button links to a new `InitiateVerificationAction` controller that generates the device_id, calls VerifyID's URL generator API, and redirects the user to the returned URL.
43+
44+
**Alternatives considered**:
45+
- Pre-generating the verification URL when the checkout page renders (in the Twig runtime) — this would require an HTTP call on every page load, even if the user never clicks verify
46+
- AJAX-based URL generation — adds client-side complexity
47+
48+
**Rationale**: Lazy URL generation (only when the user clicks) avoids unnecessary API calls. The Twig runtime now generates a URL to our own controller rather than directly to the provider, keeping the template simple.
49+
50+
```
51+
Current: Template → renders direct Criipto URL → user clicks → Criipto → callback
52+
New: Template → renders internal route → user clicks → InitiateAction
53+
→ calls VerifyID API → redirects to verification URL → VerifyID → callback
54+
```
55+
56+
### 3. Callback reads token_age_verified parameter
57+
58+
**Decision**: The callback controller reads `token_age_verified` from the query string, retrieves the `device_id` from the session, and calls VerifyID's `/auth_check/{token}/{device_id}` endpoint.
59+
60+
**Rationale**: This matches VerifyID's documented flow. The response `{ "age": int|null }` maps directly to the existing `Customer::setOlderThan()` method.
61+
62+
### 4. Rename controller and route
63+
64+
**Decision**: Rename `CriiptoCallbackAction` to `VerifyIdCallbackAction`. Add `InitiateVerificationAction`. Rename routes to `setono_sylius_age_verification_shop_initiate` and `setono_sylius_age_verification_shop_callback`.
65+
66+
**Rationale**: Provider-specific naming in routes and controllers. The route names drop the `criipto` prefix to avoid coupling to any specific provider name, since the plugin is already age-verification-specific.
67+
68+
### 5. Configuration simplification
69+
70+
**Decision**: Replace the `criipto` config node (3 values) with `verify_id` node containing only `plugin_key`. Default to `%env(VERIFYID_PLUGIN_KEY)%`.
71+
72+
**Rationale**: VerifyID only requires a plugin key. No client secret, no domain discovery.
73+
74+
### 6. Remove MinimumAge cases 15 and 21
75+
76+
**Decision**: Reduce `MinimumAge` enum to `MINIMUM_16 = 16` and `MINIMUM_18 = 18`. Remove `asScope()` method (was Criipto-specific).
77+
78+
**Rationale**: VerifyID's `/url_generator_s/` endpoint only accepts ages 16 and 18. The `asScope()` method was for building OIDC scope strings, which no longer applies.
79+
80+
### 7. Dependency cleanup
81+
82+
**Decision**: Remove `firebase/php-jwt` and `cuyz/valinor` from composer.json. Add `symfony/uid` if not already available (for `Uuid::v4()`).
83+
84+
**Rationale**: JWT decoding and Valinor mapping were only used for Criipto token handling. `symfony/uid` provides UUID generation and is likely already available through Sylius dependencies.
85+
86+
## Risks / Trade-offs
87+
88+
- **[VerifyID API availability]** The plugin now depends on VerifyID's API being available for both URL generation and age checking. Criipto used a client-side redirect flow that only needed the server for token exchange. → Mitigation: Both calls are short-lived HTTP GETs. The initiate call only happens when the user clicks verify. Error handling should show a clear message if the API is unreachable.
89+
90+
- **[Session dependency]** The device_id is stored in session, which means the flow breaks if the session is lost between initiation and callback (e.g., session cookie expires, load balancer issues). → Mitigation: Standard Symfony session handling; same risk as any CSRF-token-based flow.
91+
92+
- **[Age 16/18 only]** Removing ages 15 and 21 is a breaking change for host apps that configured products with those minimum ages. → Mitigation: This is a major version bump (2.x branch). Document the migration clearly.
93+
94+
- **[No test mode toggle]** VerifyID has a test key (`verifyid_test`) and test endpoint (`/auth_check_test/`). We're not building an explicit test mode toggle. → Mitigation: Users can set `VERIFYID_PLUGIN_KEY=verifyid_test` for testing. The `/auth_check_test/` endpoint can be addressed later if needed.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
## Why
2+
3+
Replace Criipto (OpenID Connect-based) age verification provider with VerifyID (REST API-based). VerifyID provides a simpler integration model — no OIDC discovery, no JWT decoding, no client secrets — just a plugin key and two REST endpoints. This also aligns the plugin with MitID-based verification via VerifyID's platform.
4+
5+
## What Changes
6+
7+
- **BREAKING**: Remove all Criipto/OIDC integration (OpenID configuration discovery, JWT token decoding, JWKS key fetching)
8+
- **BREAKING**: Replace `criipto` configuration block (`client_id`, `client_secret`, `verify_domain`) with `verify_id` configuration (`plugin_key`)
9+
- **BREAKING**: Remove `MinimumAge` cases for ages 15 and 21 — VerifyID only supports 16 and 18
10+
- Replace direct-link-to-provider flow with two-phase server-side flow: user clicks link to own controller, controller calls VerifyID API to get verification URL, redirects user there
11+
- New callback controller reads `token_age_verified` query parameter (instead of OIDC authorization code) and calls VerifyID `/auth_check/` endpoint to get verified age
12+
- Generate `device_id` (UUID v4) server-side and store in session — no client-side JavaScript required
13+
- Rename route from `criipto_callback` to provider-agnostic name
14+
15+
## Capabilities
16+
17+
### New Capabilities
18+
- `verifyid-integration`: Server-side integration with VerifyID REST API for age verification (URL generation, callback handling, age checking)
19+
20+
### Modified Capabilities
21+
- None
22+
23+
## Impact
24+
25+
- **Deleted files**: `OpenIdConfiguration`, `OpenIdConfigurationFactory`, `OpenIdConfigurationFactoryInterface`, `TokenDecoder`, `TokenDecoderInterface`, `DecodedToken`
26+
- **Rewritten files**: `CriiptoCallbackAction` (renamed), `AuthorizationUrlGenerator`, `Configuration`, `SetonoSyliusAgeVerificationExtension`, `services.xml`, routes, shop template
27+
- **Modified files**: `MinimumAge` enum (remove 15/21 cases, remove `asScope()`)
28+
- **Dependencies**: Can remove `firebase/php-jwt` and potentially `cuyz/valinor` (used only for JWT token mapping). May need `symfony/uid` for UUID v4 generation.
29+
- **Environment variables**: Remove `CRIIPTO_CLIENT_ID`, `CRIIPTO_CLIENT_SECRET`, `CRIIPTO_VERIFY_DOMAIN`. Add `VERIFYID_PLUGIN_KEY`.
30+
- **Session**: New dependency on session storage for `device_id` persistence across the redirect flow
31+
- **Breaking for host apps**: Config key changes, env var changes, `MinimumAge` enum cases removed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
## ADDED Requirements
2+
3+
### Requirement: Initiate age verification via VerifyID
4+
The system SHALL provide a controller action that initiates the VerifyID age verification flow. It SHALL generate a UUID v4 device_id, store it in the Symfony session, call the VerifyID `/url_generator_s/{pluginKey}/{device_id}/{age}?domain={callbackUrl}` endpoint, and redirect the user to the returned verification URL.
5+
6+
#### Scenario: Successful verification initiation
7+
- **WHEN** a user clicks the "Verify Age" button on the checkout page with a minimum age of 18
8+
- **THEN** the system generates a UUID v4 device_id, stores it in the session, calls `GET https://api.verifyid.dk/api/url_generator_s/{pluginKey}/{device_id}/18?domain={callbackUrl}`, and redirects the user to the URL returned in the response
9+
10+
#### Scenario: Successful verification initiation for age 16
11+
- **WHEN** a user clicks the "Verify Age" button on the checkout page with a minimum age of 16
12+
- **THEN** the system calls the VerifyID endpoint with age parameter `16` and redirects to the returned URL
13+
14+
#### Scenario: VerifyID API returns error
15+
- **WHEN** the VerifyID URL generator endpoint returns a non-200 response or returns `url: null`
16+
- **THEN** the system redirects the user back to the checkout complete page (does not crash)
17+
18+
### Requirement: Handle VerifyID callback
19+
The system SHALL provide a callback controller action at a public route. When VerifyID redirects the user back after verification, the callback SHALL read the `token_age_verified` query parameter, retrieve the `device_id` from the session, call the VerifyID `/auth_check/{token}/{device_id}` endpoint, and store the verified age on the customer.
20+
21+
#### Scenario: Successful age verification callback
22+
- **WHEN** the user is redirected back with a valid `token_age_verified` query parameter
23+
- **THEN** the system calls `GET https://api.verifyid.dk/api/auth_check/{token}/{device_id}`, receives `{ "age": 18 }`, sets `olderThan = 18` and `ageCheckedAt = now` on the customer, and redirects to the checkout complete page
24+
25+
#### Scenario: Callback with null age (verification failed)
26+
- **WHEN** the VerifyID `/auth_check/` endpoint returns `{ "age": null }`
27+
- **THEN** the system does NOT update the customer's age verification fields, and redirects to the checkout complete page (the age check on the checkout page will still show the verification prompt)
28+
29+
#### Scenario: Callback without token parameter
30+
- **WHEN** the callback URL is accessed without a `token_age_verified` query parameter
31+
- **THEN** the system redirects to the checkout complete page without updating any customer data
32+
33+
#### Scenario: Callback without device_id in session
34+
- **WHEN** the callback is accessed but no `device_id` exists in the session
35+
- **THEN** the system redirects to the checkout complete page without updating any customer data
36+
37+
### Requirement: Plugin configuration with VerifyID credentials
38+
The system SHALL accept a `verify_id` configuration block with a `plugin_key` setting. The `plugin_key` SHALL default to the `VERIFYID_PLUGIN_KEY` environment variable.
39+
40+
#### Scenario: Configuration with environment variable
41+
- **WHEN** the plugin is configured without explicitly setting `plugin_key`
42+
- **THEN** the system uses the value of `%env(VERIFYID_PLUGIN_KEY)%`
43+
44+
#### Scenario: Configuration with explicit value
45+
- **WHEN** the plugin configuration sets `verify_id.plugin_key: "my_key"`
46+
- **THEN** the system uses `"my_key"` as the VerifyID plugin key
47+
48+
### Requirement: Only ages 16 and 18 are supported
49+
The `MinimumAge` enum SHALL only contain cases for ages 16 and 18.
50+
51+
#### Scenario: Product with minimum age 16
52+
- **WHEN** a product has minimum age set to 16
53+
- **THEN** the system initiates verification with age parameter 16
54+
55+
#### Scenario: Product with minimum age 18
56+
- **WHEN** a product has minimum age set to 18
57+
- **THEN** the system initiates verification with age parameter 18
58+
59+
### Requirement: Server-side device_id generation
60+
The system SHALL generate the VerifyID device_id server-side as a UUID v4 string, without requiring any client-side JavaScript. The device_id SHALL be stored in the Symfony session and reused for the auth_check call after callback.
61+
62+
#### Scenario: Device ID persists across redirect
63+
- **WHEN** the initiation controller generates a device_id and stores it in the session
64+
- **THEN** the callback controller retrieves the same device_id from the session to use in the `/auth_check/` API call
65+
66+
### Requirement: Twig runtime generates internal route URL
67+
The Twig runtime `authorizationUrl()` method SHALL return a URL to the internal initiation controller (not directly to VerifyID). The initiation controller route SHALL include the minimum age as a parameter.
68+
69+
#### Scenario: Template renders verify button
70+
- **WHEN** the checkout complete page renders and an age check is required for age 18
71+
- **THEN** the "Verify Age" link points to the internal initiation route with age 18 as a parameter
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
## 1. Delete Criipto/OIDC components
2+
3+
- [x] 1.1 Delete `src/OpenIdConfiguration/OpenIdConfiguration.php`, `OpenIdConfigurationFactory.php`, `OpenIdConfigurationFactoryInterface.php`
4+
- [x] 1.2 Delete `src/Token/TokenDecoder.php`, `TokenDecoderInterface.php`, `DecodedToken.php`
5+
- [x] 1.3 Delete `src/Controller/CriiptoCallbackAction.php`
6+
7+
## 2. Update MinimumAge enum
8+
9+
- [x] 2.1 Remove `MINIMUM_15` and `MINIMUM_21` cases from `MinimumAge` enum, remove `asScope()` method
10+
11+
## 3. Update configuration
12+
13+
- [x] 3.1 Replace `criipto` config node with `verify_id` node containing `plugin_key` (defaulting to `%env(VERIFYID_PLUGIN_KEY)%`) in `Configuration.php`
14+
- [x] 3.2 Update `SetonoSyliusAgeVerificationExtension` to read `verify_id.plugin_key` and set corresponding container parameter
15+
16+
## 4. Create VerifyID controllers
17+
18+
- [x] 4.1 Create `InitiateVerificationAction` controller: generates UUID v4 device_id, stores in session, calls VerifyID `/url_generator_s/` endpoint, redirects to returned URL
19+
- [x] 4.2 Create `VerifyIdCallbackAction` controller: reads `token_age_verified` query param, retrieves device_id from session, calls `/auth_check/` endpoint, stores verified age on customer
20+
21+
## 5. Update URL generator
22+
23+
- [x] 5.1 Rewrite `AuthorizationUrlGenerator` to generate an internal route URL (to `InitiateVerificationAction`) with the minimum age as a parameter, instead of building an external OIDC URL
24+
- [x] 5.2 Update `AuthorizationUrlGeneratorInterface` if the signature changes
25+
26+
## 6. Update service wiring
27+
28+
- [x] 6.1 Update `services.xml`: remove Criipto/OIDC service definitions, add new controller services (`InitiateVerificationAction`, `VerifyIdCallbackAction`) with required dependencies (HttpClient, session, router, plugin_key parameter)
29+
- [x] 6.2 Update `AuthorizationUrlGenerator` service definition (remove OpenIdConfiguration dependency, simplify to just router)
30+
31+
## 7. Update routes
32+
33+
- [x] 7.1 Update `routes/global.yaml`: replace `criipto_callback` route with two routes — `setono_sylius_age_verification_shop_initiate` (with age parameter) and `setono_sylius_age_verification_shop_callback`
34+
35+
## 8. Update Twig runtime
36+
37+
- [x] 8.1 Update `Twig/Runtime::authorizationUrl()` — it no longer needs `countryCode` since the URL is now an internal route. Simplify to pass just the `MinimumAge` to the generator.
38+
39+
## 9. Update dependencies
40+
41+
- [x] 9.1 Remove `firebase/php-jwt` and `cuyz/valinor` from `composer.json`, add `symfony/uid` if not already present
42+
- [x] 9.2 Run `composer update` to verify dependency changes
43+
44+
## 10. Update tests and tooling
45+
46+
- [x] 10.1 Update or rewrite existing unit tests for changed components (`MinimumAgeChecker`, `AuthorizationUrlGenerator`, etc.)
47+
- [x] 10.2 Add unit tests for `InitiateVerificationAction` and `VerifyIdCallbackAction`
48+
- [x] 10.3 Update test application config (`tests/Application/`) — replace Criipto env vars with VerifyID plugin key
49+
- [x] 10.4 Run `composer analyse` (PHPStan) and fix any errors
50+
- [x] 10.5 Run `composer check-style` and fix any coding standard violations
51+
- [x] 10.6 Run `vendor/bin/composer-dependency-analyser` and fix any issues

0 commit comments

Comments
 (0)