omniauth-vitsme 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a95f27003ba5e85db4705861d35ebbb62a8d4701a7494d01faec43144d4f4749
4
- data.tar.gz: ab6049d8756893307f0076cc75c2f14f83042de57f292f1031ab052dfbec922e
3
+ metadata.gz: aa0610f9d0d2e8dd9f3712a4f5b071a5fd680d2218e8b6155e3383d95c191da1
4
+ data.tar.gz: cdf33cdcb3b192bd2ba1e05dd9836d4078fe9da5caa221b370746ebba2fd146a
5
5
  SHA512:
6
- metadata.gz: 0dcb9ba6a03dac71969ed404f4e41d47cc47d21b2ca710ea55b36720afdbe11e8750347141f8cc6abdfaebfb65c95154be52655181626b7ae8740aa944580d82
7
- data.tar.gz: e0980bf755901669c8c3eb2ea70dc4ab79532a6ee4adad032f940ffaa7ed19a8035f479197002bab62bbb41c47e912be2bd6085a77e9bdf1bf5386f4450a1594
6
+ metadata.gz: f317e9bafb4ad03afcb8bbbe582e322c4d2da73f5adb9d30a11c44b04aa2b1913f0ff9350c1450bc5a7e48583339084e45c6248baecca57d27c1fed01379af87
7
+ data.tar.gz: 1706fb04b709cc4577f76095c2416b58a767c24e2feab20341a2a78fe7c0a0cc41205e10448ea7aecd0ddfd18645f07337ea703b54057275f993088b366a3ce8
data/AGENTS.md ADDED
@@ -0,0 +1,61 @@
1
+ # AGENTS.md — omniauth-vitsme
2
+
3
+ You are helping someone sign people in with vits.me, or ask vits.me about them, from a Ruby/Rails
4
+ app. These rules are not style preferences: breaking one leaks a private identifier or credits one
5
+ person's verified details to another.
6
+
7
+ ## Choosing what to ask for
8
+
9
+ - Scopes, claims, types, value formats and gates are all in [data/claims.json](data/claims.json).
10
+ Read it instead of guessing; an unknown scope raises at boot.
11
+ - Ask for the least that answers the question: `age__over_18` rather than `birthdate`, a gate
12
+ rather than `nationality` when "is this person Israeli?" is all you need.
13
+ - vits.me has no email, phone, picture or username claim. Ask the person for anything else.
14
+
15
+ ## Rules
16
+
17
+ 1. **The uid is private.** `auth.uid` (`sub`), `extra.lookup_uids` and `extra.fluid_uids` are
18
+ per-site secrets. Store them server-side as the lookup key, and never render, log, put in a URL,
19
+ federate, or derive a handle or email from them.
20
+ 2. **Don't log the auth hash.** `request.env["omniauth.auth"]` holds the uid and every shared detail.
21
+ 3. **Follow-up questions about a signed-in person need `expected_uid`.** A provider that verifies a
22
+ name, an age or a nationality of someone already signed in to the app must set
23
+ `expected_uid: ->(env) { ...the signed-in person's stored uid... }`. Without it, whoever signs in
24
+ at vits.me gets their details credited to the signed-in account. Don't set it on the sign-in
25
+ provider: there is nobody to expect yet.
26
+ 4. **Missing is not false.** Any disclosure claim may be absent: not shared, not held, or hidden.
27
+ Branch on `true` / `false` / absent, and never default a missing boolean to `false`.
28
+ 5. **A gate emits nothing.** `nationality_gate:` admits only verified nationals. Success is the
29
+ answer, and a refusal is `:nationality_gate_refused`. It can't be combined with the `fluid` scope.
30
+ 6. **`fluid_` means unverified.** Without the `fluid` scope you never see one. With it, give such
31
+ people a limited, tentative account and merge it when `extra.fluid_uids` shows up on a verified
32
+ sign-in.
33
+ 7. **Read everything in the callback.** Tokens live 5 seconds and there is no refresh token. Nothing
34
+ can be fetched later.
35
+ 8. **Find returning people by `extra.lookup_uids`** (plus `fluid_uids` with FLUID), current first, and
36
+ move a record found under an older uid to `auth.uid`.
37
+ 9. **User-facing copy says "verified"**, not "vits.me-verified". There is no need to name vits.me to
38
+ end users.
39
+ 10. **Handle every failure type** in [README.md#failure-types](README.md#failure-types):
40
+ `not_signed_in`, `identity_mismatch`, `unverified_identity`, `nationality_gate_refused`,
41
+ `access_denied`.
42
+
43
+ ## Doing the work
44
+
45
+ - Rails: `bin/rails g omniauth_vitsme:install` (see `--help`) writes the initializer, controller,
46
+ locales, button and migration correctly. Prefer it to hand-writing.
47
+ - Tests: `require "omniauth/vitsme/test"`, then `OmniAuth::Vitsme::Test.mock_auth(...)` /
48
+ `.mock_failure(...)`. Mocks have the real shape; unknown claims raise.
49
+ - A worked app: [examples/rails/app.rb](examples/rails/app.rb).
50
+ - The skill [.claude/skills/vitsme-integrate/SKILL.md](.claude/skills/vitsme-integrate/SKILL.md) walks
51
+ through adding vits.me to an app end to end.
52
+
53
+ ## Working on this gem
54
+
55
+ - `bundle exec rspec && bundle exec rubocop` must pass. The suite boots the example Rails app.
56
+ - `data/claims.json` mirrors the vits.me server (`config/initializers/doorkeeper*.rb` in the
57
+ mattyr mono-repo). Change them together, and keep `spec/omniauth/vitsme/catalog_spec.rb`'s scope
58
+ list in step.
59
+ - `AuthShape` builds the auth hash for both the strategy and the test helper. Change the shape there
60
+ only.
61
+ - Pre-1.0: breaking changes are fine. Record them in CHANGELOG.md.
data/CHANGELOG.md CHANGED
@@ -5,6 +5,57 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.1] - 2026-11-02
9
+
10
+ ### Fixed
11
+
12
+ - The docs claimed OmniAuth's test mode skips the identity checks. It doesn't: test mode skips the
13
+ trip to vits.me, but `expected_uid` and `require_verified` still judge the mocked answer. The
14
+ README, the test helper and the skill now say so, and four specs pin the behaviour. The examples
15
+ mock someone else to get `:identity_mismatch` and keep `mock_failure` for refusals only vits.me
16
+ makes.
17
+
18
+ ## [0.2.0] - 2026-11-02
19
+
20
+ A rewrite that brings the gem up to the vits.me platform. **Breaking:** the auth hash changed shape,
21
+ and Ruby ≥ 3.3 is required.
22
+
23
+ ### Added
24
+
25
+ - `expected_uid:` for follow-up questions about a person already signed in. vits.me is told whom to
26
+ expect (OIDC `claims` sub value) and pre-selects that person. The callback refuses anyone else
27
+ (`:identity_mismatch`) and fails early when nobody is signed in (`:not_signed_in`). Earlier SOLIDs
28
+ listed in `defunct_solids` still match.
29
+ - `require_verified:` refuses pending (`fluid_`) identities (`:unverified_identity`). It is on by
30
+ default with `expected_uid`.
31
+ - `nationality_gate: %w[IL]` adds `openid_nationality__xx` gate scopes. A gate refusal is
32
+ `:nationality_gate_refused`, read from vits.me's machine-readable `vitsme_error`. Combining a gate
33
+ with `fluid` raises at boot.
34
+ - `environment:` (`:production` / `:staging` / `:development`) and `OmniAuth::Vitsme.issuer`. The host
35
+ is derived from the issuer.
36
+ - `data/claims.json`: a machine-readable catalog of every scope, claim, type, gate and refusal.
37
+ `scope:` is validated against it at boot.
38
+ - `extra.claims`: the shared claims, typed (booleans, integers). `extra.identity` (`:solid` / `:fluid`),
39
+ `extra.lookup_uids` (current + defunct) and `extra.fluid_uids`. `info.verified`.
40
+ - PKCE (S256) and `prompt: :login` by default. `redirect_uri` defaults to the provider's callback.
41
+ - `OmniAuth::Vitsme::Test` (`mock_auth`, `mock_failure`), which builds mocks with the real shape.
42
+ - `bin/rails g omniauth_vitsme:install`: initializer, callbacks controller, en/he messages, sign-in
43
+ button, routes and a `vitsme_uid` migration.
44
+ - `examples/rails/app.rb`, booted by the test suite. AGENTS.md, llms.txt and a Claude Code skill
45
+ (`.claude/skills/vitsme-integrate`).
46
+
47
+ ### Changed
48
+
49
+ - `info` holds only what vits.me sends: `name`, `nationality`, `verified`. `credentials` holds
50
+ `id_token` and `scope` (vits.me issues no refresh token and its access token lapses in 5 s).
51
+
52
+ ### Removed
53
+
54
+ - `info.first_name`, `last_name`, `email`, `birthday`, `age_over_*`: vits.me never sent the first
55
+ three. `birthday` conflated `birthdate` with a month-day value. The `age_over_*` flags passed
56
+ `"false"` through as a truthy string. Use `extra.claims`.
57
+ - Ruby 3.0–3.2 support.
58
+
8
59
  ## [0.1.0] - 2026-02-11
9
60
 
10
61
  ### Added
data/README.md CHANGED
@@ -1,182 +1,280 @@
1
1
  # OmniAuth vits.me
2
2
 
3
- OmniAuth strategy for [vits.me](https://vits.me) (itsme), global digital identity verification platform.
4
-
5
- Wraps [omniauth_openid_connect](https://github.com/omniauth/omniauth_openid_connect) with vits.me-specific defaults so you only need to supply `client_id` and `client_secret`.
6
-
7
- ## Installation
8
-
9
- Add to your Gemfile:
3
+ Sign people in with [vits.me](https://vits.me), ask about details they have verified there (a name, an
4
+ age, a nationality), and admit only verified nationals of chosen countries. An OmniAuth strategy on top
5
+ of [omniauth_openid_connect](https://github.com/omniauth/omniauth_openid_connect), with the vits.me
6
+ rules built in.
7
+
8
+ > **Agents:** read [AGENTS.md](AGENTS.md) first. Every scope and claim is in
9
+ > [data/claims.json](data/claims.json).
10
+
11
+ - [Quick start](#quick-start)
12
+ - [Environments](#environments)
13
+ - [Claims](#claims)
14
+ - [Verifying a signed-in person](#verifying-a-signed-in-person)
15
+ - [Gated communities](#gated-communities)
16
+ - [Unverified (FLUID) identities](#unverified-fluid-identities)
17
+ - [Finding a returning person](#finding-a-returning-person)
18
+ - [Failure types](#failure-types)
19
+ - [The auth hash](#the-auth-hash)
20
+ - [Operational notes](#operational-notes)
21
+ - [Privacy rules](#privacy-rules)
22
+ - [Testing](#testing)
23
+ - [Options](#options)
24
+
25
+ ## Quick start
10
26
 
11
27
  ```ruby
12
- gem "omniauth-vitsme", "~> 0.1"
28
+ # Gemfile
29
+ gem "omniauth-vitsme", "~> 0.2"
30
+ gem "omniauth-rails_csrf_protection" # OmniAuth 2 starts sign-in by POST only
13
31
  ```
14
32
 
15
- Then run:
33
+ The generator writes everything below: an initializer, a callbacks controller, failure messages
34
+ (en/he), a sign-in button, routes, and a migration adding a unique `vitsme_uid` column.
16
35
 
17
36
  ```
18
- bundle install
37
+ bin/rails g omniauth_vitsme:install [--model=User] [--scope=name] [--verify=age__over_18] [--gate=IL] [--fluid]
19
38
  ```
20
39
 
21
- ## Usage
22
-
23
- ### Rails (Rack middleware)
40
+ Or by hand:
24
41
 
25
42
  ```ruby
26
43
  Rails.application.config.middleware.use OmniAuth::Builder do
27
44
  provider :vitsme,
28
45
  client_id: ENV["VITSME_CLIENT_ID"],
29
- client_secret: ENV["VITSME_CLIENT_SECRET"]
46
+ client_secret: ENV["VITSME_CLIENT_SECRET"],
47
+ environment: Rails.env.production? ? :production : :development
30
48
  end
31
49
  ```
32
50
 
33
- ### With additional scopes
51
+ ```erb
52
+ <%= button_to "Sign in", "/auth/vitsme", method: :post, data: { turbo: false } %>
53
+ ```
54
+
55
+ Client credentials come from the vits.me team: there is no self-service registration yet. Register
56
+ `https://<your host>/auth/vitsme/callback` (and one callback per extra provider) as redirect URIs.
57
+
58
+ ## Environments
59
+
60
+ | `environment:` | Issuer |
61
+ |---|---|
62
+ | `:production` (default) | `https://vits.me` |
63
+ | `:staging` | `https://staging.vits.me` |
64
+ | `:development` / `"test"` | `https://vitsme.localhost` (local, mkcert certificate) |
65
+
66
+ `OmniAuth::Vitsme.issuer(Rails.env)` gives the same mapping. An explicit `issuer:` wins.
67
+
68
+ ## Claims
69
+
70
+ Ask for a claim by its scope: `scope: %i[openid age__over_18 nationality]`. `openid` is always added.
71
+ An unknown scope raises `ArgumentError` when the app boots. The full list, with types and value
72
+ formats, is [data/claims.json](data/claims.json).
73
+
74
+ | Scope = claim | Type in `extra.claims` | Example |
75
+ |---|---|---|
76
+ | `name` | String: one name at the detail the person chose | `"Jane"`, `"J. D."`, `"Jane Doe"` |
77
+ | `name__full` | String | `"Jane Doe"` |
78
+ | `nationality` | String, ISO 3166-1 alpha-2 | `"IL"` |
79
+ | `nationality__in_eu`, `__in_eea`, `__in_esm`, `__gdpr_compliant` | Boolean | `true` |
80
+ | `il_government_id_number` | String | (sensitive: ask only with a legal need) |
81
+ | `age` | String: a threshold, generation, range or years | `"over_18"`, `"gen_x"`, `"25_to_34"`, `"37"` |
82
+ | `age__years` | Integer | `37` |
83
+ | `age__range` | String | `"25_to_34"` |
84
+ | `age__over_13`, `__over_18`, `__over_21` | Boolean | `true` |
85
+ | `birthdate` | String `YYYY-MM-DD` | `"1990-01-15"` |
86
+ | `birthday` | String `M-D` or `YYYY-MM-DD`, as the person chose | `"1-15"` |
87
+ | `birthyear` | String `YYYY` | `"1990"` |
88
+
89
+ **The person chooses.** On vits.me's consent screen the person picks what to share for each scope,
90
+ at the level of detail they prefer, or nothing at all.
91
+
92
+ **Missing is not false.** A claim may be absent even when its scope was granted: the person shared
93
+ nothing, holds no such detail, or hid it. `extra.claims["age__over_18"]` is `true`, `false`, or not
94
+ there. Treat "not there" as "unknown", never as "no":
34
95
 
35
96
  ```ruby
36
- Rails.application.config.middleware.use OmniAuth::Builder do
37
- provider :vitsme,
38
- client_id: ENV["VITSME_CLIENT_ID"],
39
- client_secret: ENV["VITSME_CLIENT_SECRET"],
40
- scope: [:openid, :name, :age__over_18, :nationality]
97
+ case auth.extra.claims["age__over_18"]
98
+ when true then admit
99
+ when false then refuse
100
+ else ask_them_to_share_it
41
101
  end
42
102
  ```
43
103
 
44
- ### Devise integration
104
+ There is no email, phone, picture or username. If you need a public handle, ask the person for one.
105
+
106
+ ## Verifying a signed-in person
107
+
108
+ vits.me signs in **whoever is at its keyboard** and answers about them. A provider that asks about the
109
+ person *already signed in to your app* must make sure the answer is about them, or one person's
110
+ verified details get credited to another. `expected_uid` does that:
45
111
 
46
112
  ```ruby
47
- Devise.setup do |config|
48
- config.omniauth :vitsme,
49
- client_id: ENV["VITSME_CLIENT_ID"],
50
- client_secret: ENV["VITSME_CLIENT_SECRET"],
51
- scope: [:openid, :name]
52
- end
113
+ provider :vitsme,
114
+ name: :vitsme_verify,
115
+ client_id: ENV["VITSME_CLIENT_ID"], client_secret: ENV["VITSME_CLIENT_SECRET"],
116
+ scope: %i[openid age__over_18],
117
+ expected_uid: ->(env) { User.find_by(id: env["rack.session"]["user_id"])&.vitsme_uid }
53
118
  ```
54
119
 
55
- ## Available Scopes
56
-
57
- | Scope | Description |
58
- |---|---|
59
- | `openid` | Required. Returns the subject identifier (`sub`). |
60
- | `name` | Given name and family name |
61
- | `name__full` | Full name as a single string |
62
- | `nationality` | ISO country code |
63
- | `nationality__in_eu` | Whether the user is an EU national |
64
- | `birthday` / `birthdate` | Date of birth |
65
- | `birthyear` | Year of birth |
66
- | `age__over_13` | Age verification (13+) |
67
- | `age__over_18` | Age verification (18+) |
68
- | `age__over_21` | Age verification (21+) |
120
+ - Nobody signed in: the request fails `:not_signed_in` without leaving your app.
121
+ - vits.me is told whom you expect (the standard OpenID Connect `claims` sub request). It opens the
122
+ pincode page on that person's account and refuses anyone else (`:identity_mismatch`).
123
+ - The callback checks again, in the middleware, before your controller runs: the answer must be about
124
+ `expected_uid` or a uid vits.me lists in `defunct_solids`. Otherwise it fails `:identity_mismatch`.
125
+ - With `expected_uid` set, a pending (`fluid_`) identity fails `:unverified_identity`
126
+ (`require_verified: false` turns that off).
69
127
 
70
- ## Auth Hash
128
+ Use `expected_uid` only for these follow-up questions. At sign-in there is nobody to expect yet.
71
129
 
72
- The auth hash returned in the callback looks like:
130
+ ## Gated communities
73
131
 
74
132
  ```ruby
75
- {
76
- provider: "vitsme",
77
- uid: "pairwise-subject-identifier",
78
- info: {
79
- name: "Jane Doe",
80
- first_name: "Jane",
81
- last_name: "Doe",
82
- email: "jane@example.com",
83
- birthday: "1990-01-15",
84
- nationality: "IL",
85
- age_over_13: true,
86
- age_over_18: true,
87
- age_over_21: true
88
- },
89
- credentials: {
90
- id_token: "eyJ...",
91
- token: "access_token_value",
92
- refresh_token: "refresh_token_value",
93
- expires_in: 3600,
94
- scope: "openid name"
95
- },
96
- extra: {
97
- raw_info: {
98
- sub: "pairwise-subject-identifier",
99
- name: "Jane Doe",
100
- # ... all raw claims from the userinfo endpoint
101
- }
102
- }
103
- }
133
+ provider :vitsme, client_id: ..., client_secret: ..., nationality_gate: %w[IL]
104
134
  ```
105
135
 
106
- Only requested scopes will appear in `info`. Nil values are omitted via `.compact`.
136
+ Only verified people holding one of the listed nationalities finish signing in. Several codes mean
137
+ any of them. vits.me asks the person to consent to this check. **The gate sends no claim back**: a
138
+ successful sign-in is the answer. Add the `nationality` scope if you also want the value. A person
139
+ who doesn't pass returns as `:nationality_gate_refused`, and no account is created.
107
140
 
108
- Note: vits.me uses **pairwise subject identifiers** -- the same person will have a different `uid` for each registered client application.
141
+ A gate admits verified people only, so it can't be combined with the `fluid` scope (that raises at
142
+ boot). Make your gated sign-in button look different from an ordinary one, so people know before they
143
+ start. To build a community that way, follow madon's lead: gate the sign-in, then ask the new member
144
+ to pick a public handle. Never derive the handle from the uid.
109
145
 
110
- ## Configuration
146
+ ## Unverified (FLUID) identities
111
147
 
112
- This gem is a wrapper for [omniauth_openid_connect](https://github.com/omniauth/omniauth_openid_connect). All configuration options available for `omniauth_openid_connect` can be used with `omniauth-vitsme`. The most common options are listed below.
148
+ By default only verified people can sign in. With `scope: %i[openid fluid]`, people whose verification
149
+ is still pending can sign in too:
113
150
 
114
- | Option | Default | Description |
115
- |---|---|---|
116
- | `client_id` | *required* | Your vits.me client ID |
117
- | `client_secret` | *required* | Your vits.me client secret |
118
- | `scope` | `[:openid]` | OIDC scopes to request |
119
- | `discovery` | `true` | Fetch endpoints from `.well-known/openid-configuration` |
120
- | `issuer` | `https://vits.me` | OIDC issuer URL |
121
- | `response_type` | `code` | OAuth response type (Authorization Code Flow) |
151
+ - `uid` starts with `fluid_`, `extra.identity` is `:fluid`, and `info.verified` is `false`.
152
+ - A pending identity lasts one week. Give it a limited, tentative account.
153
+ - When the person is verified, a later sign-in returns their `solid_` uid with the pending uids in
154
+ `extra.fluid_uids`. Merge the tentative account into the verified one, and drop tentative accounts
155
+ nobody claimed after a week.
122
156
 
123
- ## Environments
157
+ ## Finding a returning person
124
158
 
125
- For development or staging environments, Vits.me may provide a separate set of endpoints. You can configure these by overriding the `client_options`.
159
+ Look a person up by `extra.lookup_uids`, current uid first, plus `extra.fluid_uids` if you use FLUID.
160
+ A record found under an older uid belongs to this person: move it to `auth.uid`.
126
161
 
127
162
  ```ruby
128
- Rails.application.config.middleware.use OmniAuth::Builder do
129
- provider :vitsme,
130
- name: 'vitsme_staging', # Give it a unique name
131
- client_id: ENV["VITSME_STAGING_CLIENT_ID"],
132
- client_secret: ENV["VITSME_STAGING_CLIENT_SECRET"],
133
- client_options: {
134
- site: 'https://staging.vits.me',
135
- authorize_url: 'https://staging.vits.me/oauth/authorize',
136
- token_url: 'https://staging.vits.me/oauth/token',
137
- userinfo_endpoint: 'https://staging.vits.me/userinfo'
138
- }
139
- end
163
+ uids = auth.extra.lookup_uids + auth.extra.fluid_uids
164
+ user = User.where(vitsme_uid: uids).min_by { |u| uids.index(u.vitsme_uid) }
165
+ user&.update!(vitsme_uid: auth.uid) unless user&.vitsme_uid == auth.uid
166
+ user ||= User.create!(vitsme_uid: auth.uid)
140
167
  ```
141
- **Note:** The URLs in the example above are placeholders. Please refer to the official Vits.me documentation for the correct staging URLs.
142
168
 
143
- ## Authentication Failures
169
+ (vits.me lists superseded uids in `defunct_solids` for a year after a re-key.)
144
170
 
145
- If the user denies the authentication request or if there's a misconfiguration, OmniAuth will redirect to the `/auth/failure` path in your application. The `omniauth.error` and `omniauth.error.type` variables will be set in the Rack environment.
171
+ ## Failure types
146
172
 
147
- You can catch these failures by creating a `FailureEndpoint` in your application, for example, by routing `/auth/failure` to a specific controller action.
173
+ OmniAuth sends failures to `OmniAuth.config.on_failure` (in Rails, `/auth/failure?message=<type>`).
148
174
 
149
- In a Rails application, you can handle this in your `OmniAuth::FailureEndpoint`:
175
+ | Type | Meaning |
176
+ |---|---|
177
+ | `:not_signed_in` | `expected_uid` returned nothing: nobody is signed in to your app. |
178
+ | `:identity_mismatch` | The answer was about someone other than `expected_uid`, whether caught by vits.me or by the callback. |
179
+ | `:unverified_identity` | A pending (`fluid_`) identity, refused by `require_verified`. |
180
+ | `:nationality_gate_refused` | The person doesn't hold a nationality the gate requires (or is unverified). |
181
+ | `:access_denied` | Any other refusal, e.g. the person cancelled. |
182
+ | others | From omniauth_openid_connect: `:csrf_detected`, `:invalid_grant`, `:timeout`, … |
183
+
184
+ ## The auth hash
150
185
 
151
186
  ```ruby
152
- # config/initializers/omniauth.rb
153
- OmniAuth.config.on_failure = Proc.new { |env|
154
- OmniAuth::FailureEndpoint.new(env).redirect_to_failure
187
+ {
188
+ provider: "vitsme",
189
+ uid: "solid_…", # private: store, never show or log
190
+ info: { name: "Jane D.", nationality: "IL", verified: true }, # only what was shared
191
+ credentials: { id_token: "…", scope: "openid name" }, # no refresh token
192
+ extra: {
193
+ identity: :solid, # or :fluid
194
+ lookup_uids: ["solid_…", "solid_<earlier>"],
195
+ fluid_uids: [],
196
+ claims: { "name" => "Jane D.", "age__over_18" => true }, # typed, shared claims only
197
+ raw_info: { "sub" => "solid_…", … } # the UserInfo response, verbatim
198
+ }
155
199
  }
156
200
  ```
157
201
 
202
+ `info.name` is the shared `name` (or `name__full`). OmniAuth always includes the key, which may be nil.
203
+
204
+ ## Operational notes
205
+
206
+ - **Read everything in the callback.** Authorization codes, access tokens and ID tokens live for
207
+ **5 seconds** and there is no refresh token. The strategy redeems the code and fetches UserInfo at
208
+ once. Keep your server's clock in sync (NTP): a few seconds of skew fails ID-token validation.
209
+ - **Every sign-in asks for the pincode.** A regular person's vits.me session lasts one site sign-in,
210
+ so there is no single sign-on between sites and `prompt=none` cannot succeed. `prompt: :login` is the
211
+ default. A "verify" round-trip asks for the pincode too.
212
+ - **No logout endpoint.** Sign people out of your app; there's nothing to end at vits.me.
213
+ - **PKCE (S256) is on** by default, alongside the client secret.
214
+ - **Claims arrive from UserInfo, not the ID token.** The strategy handles this.
215
+
216
+ ## Privacy rules
217
+
218
+ - The uid (`sub`), `lookup_uids` and `fluid_uids` are **private, per-site identifiers**. Treat them
219
+ like session tokens: store them server-side as the lookup key, and **never** render them, log them,
220
+ put them in a URL, federate them, or derive a username, handle or email from them.
221
+ - Don't log `request.env["omniauth.auth"]`: it holds the uid and everything the person shared.
222
+ - Ask for the least you need. `age__over_18` answers "is this an adult?". `birthdate` answers far more.
223
+ - In copy your users read, call people "verified". There is no need to name vits.me.
224
+
225
+ ## Testing
226
+
158
227
  ```ruby
159
- # app/controllers/omniauth_callbacks_controller.rb
160
- class OmniauthCallbacksController < Devise::OmniauthCallbacksController
161
- def failure
162
- # Your custom failure logic here.
163
- # For example, redirect to the root path with an error message.
164
- redirect_to root_path, alert: "Authentication failed: #{params[:message]}"
165
- end
166
- end
228
+ require "omniauth/vitsme/test"
229
+
230
+ OmniAuth.config.test_mode = true
231
+ OmniAuth::Vitsme::Test.mock_auth(claims: { age__over_18: true }) # :vitsme
232
+ OmniAuth::Vitsme::Test.mock_auth(:vitsme_verify, uid: user.vitsme_uid, claims: { name: "Jane D." })
233
+ OmniAuth::Vitsme::Test.mock_auth(identity: :fluid) # pending signup
234
+ OmniAuth::Vitsme::Test.mock_auth(:vitsme_verify, uid: someone_else_uid) # → :identity_mismatch
235
+ OmniAuth::Vitsme::Test.mock_failure(:vitsme, :nationality_gate_refused)
167
236
  ```
168
237
 
169
- The `params[:message]` will contain a description of the error (e.g., `invalid_credentials`).
238
+ Mocks are built by the same code as real sign-ins, so they have the real shape and types, and
239
+ unknown claims raise. Test mode skips the round trip to vits.me, but the strategy's identity checks
240
+ still run on the mock. A verify provider's `expected_uid` and `require_verified` judge the mocked
241
+ answer exactly as they would a real one: mock the signed-in person's uid to pass, or someone else's
242
+ to see `:identity_mismatch`. A mocked earlier uid (`defunct_uids:`) matches as it does in
243
+ production. `mock_failure` simulates refusals that only vits.me can make, such as
244
+ `:nationality_gate_refused` or `:access_denied`.
245
+
246
+ [examples/rails/app.rb](examples/rails/app.rb) is a one-file Rails app using sign-in, a gate and
247
+ verification. The test suite boots it against a stubbed vits.me.
248
+
249
+ ## Options
250
+
251
+ | Option | Default | |
252
+ |---|---|---|
253
+ | `client_id`, `client_secret` | required | From the vits.me team |
254
+ | `environment` | `:production` | `:production`, `:staging`, `:development` |
255
+ | `issuer` | from `environment` | |
256
+ | `scope` | `[:openid]` | Validated against [data/claims.json](data/claims.json) |
257
+ | `nationality_gate` | `[]` | ISO alpha-2 codes, e.g. `%w[IL]` |
258
+ | `expected_uid` | `nil` | `->(env) { uid }` for follow-up questions about a signed-in person |
259
+ | `require_verified` | on when `expected_uid` is set | Refuse `fluid_` identities |
260
+ | `send_expected_uid` | `true` | Tell vits.me whom you expect |
261
+ | `prompt` | `:login` | |
262
+ | `pkce` | `true` | S256 |
263
+ | `name` | `"vitsme"` | Set a different name for each additional provider |
264
+
265
+ Everything else is passed to omniauth_openid_connect.
170
266
 
171
267
  ## Development
172
268
 
173
269
  ```
174
270
  bundle install
175
- bundle exec rspec
271
+ bundle exec rspec # strategy, catalog, generator, and the example Rails app
176
272
  bundle exec rubocop
177
- bundle exec rake # runs both
178
273
  ```
179
274
 
275
+ The catalog mirrors the vits.me server. When vits.me adds a scope, it lands in `data/claims.json` in
276
+ the same change (vits.me's `docs/adding-vitsme-scope.md`).
277
+
180
278
  ## License
181
279
 
182
280
  MIT License. See [LICENSE.txt](LICENSE.txt).