identizer 0.1.0
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 +7 -0
- data/CHANGELOG.md +48 -0
- data/LICENSE.txt +21 -0
- data/README.md +218 -0
- data/exe/identizer +7 -0
- data/lib/identizer/app.rb +111 -0
- data/lib/identizer/authorization.rb +21 -0
- data/lib/identizer/cli.rb +95 -0
- data/lib/identizer/configuration.rb +186 -0
- data/lib/identizer/directory_entry.rb +101 -0
- data/lib/identizer/docs.rb +22 -0
- data/lib/identizer/grant_store.rb +66 -0
- data/lib/identizer/handlers/auth0.rb +32 -0
- data/lib/identizer/handlers/auth0_management.rb +66 -0
- data/lib/identizer/handlers/base.rb +91 -0
- data/lib/identizer/handlers/cognito.rb +50 -0
- data/lib/identizer/handlers/directory.rb +76 -0
- data/lib/identizer/handlers/docs.rb +19 -0
- data/lib/identizer/handlers/login.rb +81 -0
- data/lib/identizer/handlers/oidc.rb +113 -0
- data/lib/identizer/handlers/overview.rb +19 -0
- data/lib/identizer/handlers/saml.rb +143 -0
- data/lib/identizer/handlers/settings.rb +22 -0
- data/lib/identizer/identity.rb +39 -0
- data/lib/identizer/identity_store/sqlite_store.rb +63 -0
- data/lib/identizer/identity_store.rb +86 -0
- data/lib/identizer/ldap/filter.rb +58 -0
- data/lib/identizer/ldap/handler.rb +66 -0
- data/lib/identizer/ldap/server.rb +178 -0
- data/lib/identizer/ldap.rb +16 -0
- data/lib/identizer/providers.rb +54 -0
- data/lib/identizer/renderer.rb +52 -0
- data/lib/identizer/responses.rb +46 -0
- data/lib/identizer/saml/encryptor.rb +66 -0
- data/lib/identizer/saml/keypair.rb +53 -0
- data/lib/identizer/saml/response_builder.rb +138 -0
- data/lib/identizer/saml/signer.rb +96 -0
- data/lib/identizer/saml.rb +17 -0
- data/lib/identizer/server.rb +134 -0
- data/lib/identizer/tls.rb +61 -0
- data/lib/identizer/token_minter.rb +89 -0
- data/lib/identizer/version.rb +5 -0
- data/lib/identizer/web/views/directory/index.html.erb +69 -0
- data/lib/identizer/web/views/docs/broker-app.html.erb +67 -0
- data/lib/identizer/web/views/docs/cognito.html.erb +22 -0
- data/lib/identizer/web/views/docs/getting-started.html.erb +28 -0
- data/lib/identizer/web/views/docs/index.html.erb +9 -0
- data/lib/identizer/web/views/docs/ldap.html.erb +38 -0
- data/lib/identizer/web/views/docs/oidc.html.erb +40 -0
- data/lib/identizer/web/views/docs/saml.html.erb +52 -0
- data/lib/identizer/web/views/docs/tls.html.erb +29 -0
- data/lib/identizer/web/views/docs/troubleshooting.html.erb +25 -0
- data/lib/identizer/web/views/layout.html.erb +58 -0
- data/lib/identizer/web/views/login.html.erb +19 -0
- data/lib/identizer/web/views/overview/index.html.erb +40 -0
- data/lib/identizer/web/views/settings/index.html.erb +28 -0
- data/lib/identizer.rb +64 -0
- metadata +282 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 76e9316e637b4f310e3bd6a3de54774b7fd22ca365ae04942b0aae0196a218ad
|
|
4
|
+
data.tar.gz: 2c99082c990f28cabab4e57139dca3659839df1a3b6297a6f1aa3c42a175ec1d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 71bc913525b42bc3ddad2762cc5dc6f869a694c26cbfb45830a9eb74923631a158b49eba54c95e7ed6e2c26f6507d6cc136af3539f73669357174b7a8c1823e4
|
|
7
|
+
data.tar.gz: 31fbca1b65dc8b02de55d4adf1acac07c12f4e59db1a712bdae0bac0da2e3c1b8f38d534eb2d5cb398e891cd49dbc08d192b817a7b75616e5c384ff4d4718f3d
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format is based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres
|
|
5
|
+
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-06-27
|
|
10
|
+
|
|
11
|
+
First release. A local identity provider for developing and testing auth/SSO
|
|
12
|
+
integrations, extracted and decoupled from the tap-v3 SSO emulator.
|
|
13
|
+
|
|
14
|
+
### OIDC / OAuth2
|
|
15
|
+
- Authorization-code flow with PKCE (S256/plain), refresh-token grant (rotated,
|
|
16
|
+
single-use), RP-initiated logout, `nonce` in the id_token, `scope` echo.
|
|
17
|
+
- Token introspection (RFC 7662) and revocation (RFC 7009, revokes the paired
|
|
18
|
+
token). OIDC discovery + JWKS; HS256 (default) and RS256 signing.
|
|
19
|
+
- AWS Cognito hosted-UI + management-API emulation, Auth0 login + Management API
|
|
20
|
+
(`/api/v2/clients`, `/api/v2/connections`) for app provisioning/deprovisioning.
|
|
21
|
+
- Okta-style `/oauth2/v1/*` aliases for fixed-path clients; `aud` is the client_id.
|
|
22
|
+
- Optional client registry enabling `redirect_uri` / post-logout allowlists.
|
|
23
|
+
|
|
24
|
+
### SAML 2.0
|
|
25
|
+
- Real IdP: signed Response + Assertion (XML-DSig / RSA-SHA256), optional
|
|
26
|
+
EncryptedAssertion (AES-256-CBC + RSA-OAEP), metadata, SP- and IdP-initiated SSO.
|
|
27
|
+
- Attribute names default to Microsoft/WS-Fed claim URIs; fully configurable
|
|
28
|
+
(`saml_attribute_names`). Optional ACS allowlist; deflate-bomb guards.
|
|
29
|
+
|
|
30
|
+
### LDAP
|
|
31
|
+
- Simple-bind authentication and subtree search (equality / presence / substring /
|
|
32
|
+
`&` `|` `!`) over the same directory; implicit-TLS LDAPS and StartTLS.
|
|
33
|
+
|
|
34
|
+
### Directory & storage
|
|
35
|
+
- LDAP-flavoured user directory (`DirectoryEntry`) projected onto OIDC claims,
|
|
36
|
+
with arbitrary custom attributes. Pluggable identity store (default JSON
|
|
37
|
+
`ConfigStore`; optional `SqliteStore`).
|
|
38
|
+
|
|
39
|
+
### Operability & security
|
|
40
|
+
- Standalone HTTPS server + `identizer` CLI; mountable, `SCRIPT_NAME`-aware Rack
|
|
41
|
+
app. Seeded demo user, quick-start banner, request logging, `/healthz`, custom
|
|
42
|
+
domain (`--domain`).
|
|
43
|
+
- Thread-safe, TTL-enforced grant store; private keys written `0600`; uniform PKCE
|
|
44
|
+
enforcement; registered JWT claims protected from directory-attribute forging.
|
|
45
|
+
- `nokogiri` (SAML) and `net-ldap` (LDAP) load lazily; `sqlite3` is optional.
|
|
46
|
+
|
|
47
|
+
[Unreleased]: https://github.com/alex-andreiev/identizer/compare/v0.1.0...HEAD
|
|
48
|
+
[0.1.0]: https://github.com/alex-andreiev/identizer/releases/tag/v0.1.0
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alex Andreiev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Identizer
|
|
2
|
+
|
|
3
|
+
[](https://github.com/alex-andreiev/identizer/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
A local identity provider for developing and testing auth/SSO integrations.
|
|
6
|
+
|
|
7
|
+
**The problem it solves:** to test "Sign in with SSO", you normally need a real
|
|
8
|
+
Okta/Auth0/Azure/Cognito tenant, real metadata, real certificates. That's slow to
|
|
9
|
+
set up and impossible to script in CI. Identizer is a fake-but-real IdP you run
|
|
10
|
+
locally: point your app at it, sign in as a test user, done. No accounts, no cloud.
|
|
11
|
+
|
|
12
|
+
## Quick start (60 seconds)
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
gem install identizer
|
|
16
|
+
identizer # boots on https://localhost:9999
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
It prints exactly where to point your app. Open the dashboard at
|
|
20
|
+
`https://localhost:9999/` — a demo user (`demo@example.com`, password `password`)
|
|
21
|
+
is already there, so login works immediately. Then in your app's SSO settings:
|
|
22
|
+
|
|
23
|
+
- **OIDC / OpenID Connect** → issuer `https://localhost:9999` (the client reads
|
|
24
|
+
everything else from `/.well-known/openid-configuration`).
|
|
25
|
+
- **SAML** → metadata `https://localhost:9999/metadata`.
|
|
26
|
+
- **OAuth2 / Auth0** → domain `localhost:9999`.
|
|
27
|
+
|
|
28
|
+
Trigger login in your app, sign in as the demo user, and you're testing the real
|
|
29
|
+
flow. (For browser/server TLS trust, see [TLS](#tls) — one `SSL_CERT_FILE` line.)
|
|
30
|
+
|
|
31
|
+
## Which protocol do I need?
|
|
32
|
+
|
|
33
|
+
If you're not sure how your app talks to its IdP, match the setting it asks for:
|
|
34
|
+
|
|
35
|
+
| Your app's SSO config mentions… | Use | Point it at |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| "Issuer URL", "discovery", "client ID/secret", `openid` | **OIDC** | `https://localhost:9999` |
|
|
38
|
+
| "Metadata URL/XML", "ACS", "SAML", "certificate" | **SAML** | `https://localhost:9999/metadata` |
|
|
39
|
+
| "Auth0 domain", `/authorize` + `/userinfo` | **OAuth2/Auth0** | `localhost:9999` |
|
|
40
|
+
| "Cognito", "user pool", `COGNITO_ENDPOINT` | **Cognito** | `COGNITO_ENDPOINT=https://localhost:9999` |
|
|
41
|
+
| "LDAP bind", `ldap://` | **LDAP** | `identizer --ldap-port 1389` |
|
|
42
|
+
|
|
43
|
+
## How it works (two halves)
|
|
44
|
+
|
|
45
|
+
- a **directory** of sign-in identities (the pluggable "users" store), and
|
|
46
|
+
- a **provider** that accepts auth requests, signs the user in, and hands the
|
|
47
|
+
profile back over whichever protocol your app expects (OIDC, OAuth2, SAML, …).
|
|
48
|
+
|
|
49
|
+
## Why not an existing tool?
|
|
50
|
+
|
|
51
|
+
Generic OIDC mocks (e.g. `mock-oauth2-server`) are JVM/Docker, and SAML-only Ruby
|
|
52
|
+
gems (e.g. `saml_idp`) cover one protocol. Identizer is a single, zero-infra Ruby
|
|
53
|
+
gem that covers OIDC + OAuth2 + a Cognito/Auth0 **broker** with a pluggable user
|
|
54
|
+
directory — installable, mountable, and scriptable.
|
|
55
|
+
|
|
56
|
+
## Install
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
# Gemfile (development/test)
|
|
60
|
+
gem "identizer", group: %i[development test]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
bundle install
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Run standalone
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
bundle exec identizer --port 9999
|
|
71
|
+
# open https://localhost:9999/ (dashboard: identities + provider cheatsheet)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Common flags: `--port`, `--host`, `--url-host`, `--config-dir`, `--tls-cert`,
|
|
75
|
+
`--tls-key`, `--password`, `--rs256`. Anything not passed falls back to env vars
|
|
76
|
+
(`IDENTIZER_PORT`, `IDENTIZER_TLS_CERT/KEY`, `IDENTIZER_CONFIG_DIR`, …).
|
|
77
|
+
|
|
78
|
+
## Mount inside a Rack/Rails app
|
|
79
|
+
|
|
80
|
+
`Identizer::App` is a plain Rack app, so it works mounted at any path — internal
|
|
81
|
+
links honour `SCRIPT_NAME`.
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
# config.ru
|
|
85
|
+
require "identizer"
|
|
86
|
+
run Identizer.app
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
# Rails config/routes.rb (development only)
|
|
91
|
+
mount Identizer::App.new => "/idp" if Rails.env.development?
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
# RSpec / rack-test
|
|
96
|
+
require "rack/test"
|
|
97
|
+
|
|
98
|
+
app = Identizer::App.new(
|
|
99
|
+
Identizer::Configuration.new.tap do |c|
|
|
100
|
+
c.config_dir = Dir.mktmpdir
|
|
101
|
+
c.seed_identities = [{ email: "alice@example.com", claims: { given_name: "Alice" } }]
|
|
102
|
+
end
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Configure
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
Identizer.configure do |config|
|
|
110
|
+
config.port = 9999
|
|
111
|
+
config.shared_password = "password" # type this to succeed; anything else exercises the error path
|
|
112
|
+
config.signing = :rs256 # :hs256 (default) or :rs256 + JWKS for clients that verify
|
|
113
|
+
config.seed_identities = [
|
|
114
|
+
{ email: "alice@example.com", claims: { given_name: "Alice", family_name: "Doe" } }
|
|
115
|
+
]
|
|
116
|
+
# config.identity_store = MyDbBackedStore.new # plug in any object exposing #emails / #identity_for(email)
|
|
117
|
+
end
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Other options (all have sane defaults): `code_ttl` / `access_token_ttl` /
|
|
121
|
+
`refresh_token_ttl` (grant lifetimes), `clients` (registry that enables
|
|
122
|
+
`redirect_uri` allowlisting), `saml_sign_response`, `saml_encrypt_assertion` +
|
|
123
|
+
`saml_sp_certificate`, `saml_allowed_acs`, `saml_attribute_names`, `ldap_port` /
|
|
124
|
+
`ldaps_port`, and `request_logging`. See `lib/identizer/configuration.rb`.
|
|
125
|
+
|
|
126
|
+
### Identity store interface
|
|
127
|
+
|
|
128
|
+
Any object responding to this duck-typed interface can be a directory:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
#emails -> Array<String> addresses the login form accepts
|
|
132
|
+
#identity_for(email) -> Identity | nil resolve an address to an Identity
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
For full management through the web admin, a store also exposes the directory
|
|
136
|
+
interface `#entries`, `#upsert(attrs)` and `#delete(email)` (the bundled
|
|
137
|
+
`ConfigStore` and `SqliteStore` do).
|
|
138
|
+
|
|
139
|
+
The default `Identizer::IdentityStore::ConfigStore` persists identities to a JSON
|
|
140
|
+
file the dashboard writes, seeded from `config.seed_identities`.
|
|
141
|
+
|
|
142
|
+
### SQLite backend (optional)
|
|
143
|
+
|
|
144
|
+
Prefer a database? Add `gem "sqlite3"` to your Gemfile and use the bundled adapter
|
|
145
|
+
— it implements the same directory interface, so the web admin and LDAP work
|
|
146
|
+
against it unchanged:
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
bundle exec identizer --sqlite ./dev.sqlite3
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
require "identizer/identity_store/sqlite_store"
|
|
154
|
+
config.identity_store = Identizer::IdentityStore::SqliteStore.new(path: "dev.sqlite3")
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`sqlite3` is not a default dependency — JSON files remain the zero-infra default.
|
|
158
|
+
|
|
159
|
+
## Endpoints
|
|
160
|
+
|
|
161
|
+
Most clients only need the issuer/metadata URL; the rest is discovered. Okta-style
|
|
162
|
+
`/oauth2/v1/*` aliases exist for the OIDC routes (authorize/token/userinfo/keys).
|
|
163
|
+
|
|
164
|
+
| Purpose | Route |
|
|
165
|
+
|---|---|
|
|
166
|
+
| Dashboard / config | `GET /` |
|
|
167
|
+
| Health | `GET /healthz` |
|
|
168
|
+
| Login form | `GET /login`, `/authorize`, `/v1/authorize` |
|
|
169
|
+
| Cognito hosted-UI token | `POST /oauth2/token` |
|
|
170
|
+
| Auth0 token + profile | `POST /oauth/token`, `GET /userinfo` |
|
|
171
|
+
| OIDC token / logout | `POST /v1/token`, `GET /v1/logout` |
|
|
172
|
+
| OIDC introspection / revocation | `POST /introspect`, `POST /revoke` |
|
|
173
|
+
| OIDC discovery / JWKS | `GET /.well-known/openid-configuration`, `/.well-known/jwks.json` |
|
|
174
|
+
| SAML metadata / SSO | `GET /metadata`, `GET|POST /saml/sso` |
|
|
175
|
+
| Cognito management API | `POST /` with `x-amz-target` (point `COGNITO_ENDPOINT` here) |
|
|
176
|
+
| Auth0 Management API | `POST/DELETE /api/v2/clients`, `/api/v2/connections` |
|
|
177
|
+
|
|
178
|
+
## LDAP listener (optional)
|
|
179
|
+
|
|
180
|
+
Apps that authenticate via LDAP can bind and search against the same directory.
|
|
181
|
+
It's off unless you ask for it:
|
|
182
|
+
|
|
183
|
+
```sh
|
|
184
|
+
bundle exec identizer --port 9999 --ldap-port 1389
|
|
185
|
+
# ldapsearch -x -H ldap://localhost:1389 -b dc=identizer,dc=local "(mail=alice@example.com)"
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Simple bind (user DN + shared password, or anonymous) and subtree search with
|
|
189
|
+
equality / presence / substring / `&` `|` `!` filters. Entries project to
|
|
190
|
+
`uid, cn, sn, givenName, mail, ou, memberOf, objectClass`. Plain TCP + simple
|
|
191
|
+
bind — a development listener, not LDAPS.
|
|
192
|
+
|
|
193
|
+
## TLS
|
|
194
|
+
|
|
195
|
+
Login URLs must be `https` (browser popup guards reject `http`). Identizer uses a
|
|
196
|
+
provided cert (`--tls-cert/--tls-key`, ideally [mkcert](https://github.com/FiloSottile/mkcert)-generated
|
|
197
|
+
and locally trusted) or falls back to a self-signed cert written under
|
|
198
|
+
`config_dir`. For the app's server-to-server calls, trust it via
|
|
199
|
+
`export SSL_CERT_FILE=…/cert.pem`.
|
|
200
|
+
|
|
201
|
+
## SAML 2.0
|
|
202
|
+
|
|
203
|
+
A real SAML IdP: it issues **signed** assertions (XML-DSig, RSA-SHA256) verifiable
|
|
204
|
+
by standard SPs. Metadata at `/metadata`, SSO at `/saml/sso` (Redirect & POST
|
|
205
|
+
bindings), SP- and IdP-initiated. Signing uses `nokogiri`, loaded only when a
|
|
206
|
+
Response is produced. A development IdP — convenient, not hardened.
|
|
207
|
+
|
|
208
|
+
## Development
|
|
209
|
+
|
|
210
|
+
```sh
|
|
211
|
+
bin/setup # install dependencies
|
|
212
|
+
bundle exec rake # rspec + rubocop
|
|
213
|
+
bin/console # an IRB session with identizer loaded
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## License
|
|
217
|
+
|
|
218
|
+
MIT.
|
data/exe/identizer
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Identizer
|
|
4
|
+
# The Rack application. Mount it in a test harness or run it standalone via
|
|
5
|
+
# Identizer::Server. It serves three surfaces: the web admin (Overview /
|
|
6
|
+
# Directory / Settings / Docs), the runtime IdP endpoints, and the AWS Cognito
|
|
7
|
+
# management API (requests carrying x-amz-target).
|
|
8
|
+
class App
|
|
9
|
+
include Responses
|
|
10
|
+
|
|
11
|
+
Context = Struct.new(:config, :store, :minter, :codes, :refresh_tokens, :access_tokens, :renderer)
|
|
12
|
+
|
|
13
|
+
def initialize(config = Identizer.configuration)
|
|
14
|
+
@config = config
|
|
15
|
+
context = Context.new(config, config.identity_store, TokenMinter.new(config),
|
|
16
|
+
GrantStore.new, GrantStore.new, GrantStore.new, Renderer.new)
|
|
17
|
+
@overview = Handlers::Overview.new(context)
|
|
18
|
+
@directory = Handlers::Directory.new(context)
|
|
19
|
+
@settings = Handlers::Settings.new(context)
|
|
20
|
+
@docs = Handlers::Docs.new(context)
|
|
21
|
+
@login = Handlers::Login.new(context)
|
|
22
|
+
@cognito = Handlers::Cognito.new(context)
|
|
23
|
+
@auth0 = Handlers::Auth0.new(context)
|
|
24
|
+
@auth0_management = Handlers::Auth0Management.new(context)
|
|
25
|
+
@oidc = Handlers::Oidc.new(context)
|
|
26
|
+
@saml = Handlers::Saml.new(context)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def call(env)
|
|
30
|
+
request = Rack::Request.new(env)
|
|
31
|
+
target = env["HTTP_X_AMZ_TARGET"]
|
|
32
|
+
|
|
33
|
+
if target
|
|
34
|
+
@cognito.management_api(target, request)
|
|
35
|
+
else
|
|
36
|
+
route(request)
|
|
37
|
+
end
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
# Surface the failure to the console (this is a local dev tool) instead of
|
|
40
|
+
# silently swallowing it; still return a JSON 500 to the client.
|
|
41
|
+
env["rack.errors"]&.puts("[identizer] #{e.class}: #{e.message}\n #{e.backtrace&.first(8)&.join("\n ")}")
|
|
42
|
+
json(500, { error: e.message })
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def route(request)
|
|
48
|
+
admin(request) || idp(request) || auth0_management(request) ||
|
|
49
|
+
not_found("No route for #{request.request_method} #{request.path_info}")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Auth0 Management API: provision/deprovision applications and connections.
|
|
53
|
+
def auth0_management(request)
|
|
54
|
+
case [request.request_method, request.path_info]
|
|
55
|
+
in ["POST", "/api/v2/clients"] then @auth0_management.create_client(request)
|
|
56
|
+
in ["GET", "/api/v2/clients"] then @auth0_management.list_clients(request)
|
|
57
|
+
in ["POST", "/api/v2/connections"] then @auth0_management.create_connection(request)
|
|
58
|
+
in ["GET", "/api/v2/connections"] then @auth0_management.list_connections(request)
|
|
59
|
+
in ["PATCH", String => path] if path.start_with?("/api/v2/clients/")
|
|
60
|
+
@auth0_management.update_client(request, path.delete_prefix("/api/v2/clients/"))
|
|
61
|
+
in ["DELETE", String => path] if path.start_with?("/api/v2/clients/")
|
|
62
|
+
@auth0_management.delete_client(request, path.delete_prefix("/api/v2/clients/"))
|
|
63
|
+
in ["PATCH", String => path] if path.start_with?("/api/v2/connections/")
|
|
64
|
+
@auth0_management.update_connection(request, path.delete_prefix("/api/v2/connections/"))
|
|
65
|
+
in ["DELETE", String => path] if path.start_with?("/api/v2/connections/")
|
|
66
|
+
@auth0_management.delete_connection(request, path.delete_prefix("/api/v2/connections/"))
|
|
67
|
+
else nil
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Web admin surface.
|
|
72
|
+
def admin(request)
|
|
73
|
+
case [request.request_method, request.path_info]
|
|
74
|
+
in ["GET", "/healthz"] then json(200, { status: "ok", name: "identizer", version: Identizer::VERSION })
|
|
75
|
+
in ["GET", "/"] then @overview.index(request)
|
|
76
|
+
in ["GET", "/directory"] then @directory.index(request)
|
|
77
|
+
in ["POST", "/directory"] then @directory.create(request)
|
|
78
|
+
in ["POST", "/directory/delete"] then @directory.destroy(request)
|
|
79
|
+
in ["GET", "/settings"] then @settings.show(request)
|
|
80
|
+
in ["POST", "/settings"] then @settings.update(request)
|
|
81
|
+
in ["GET", "/docs"] then @docs.index(request)
|
|
82
|
+
in ["GET", String => path] if path.start_with?("/docs/")
|
|
83
|
+
@docs.show(request, path.delete_prefix("/docs/"))
|
|
84
|
+
else nil
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Runtime IdP + protocol surface.
|
|
89
|
+
def idp(request)
|
|
90
|
+
case [request.request_method, request.path_info]
|
|
91
|
+
in ["GET", "/metadata" | "/saml/metadata"] then @saml.metadata(request)
|
|
92
|
+
in ["GET" | "POST", "/saml/sso"] then @saml.sso(request)
|
|
93
|
+
in ["POST", "/saml/finish"] then @saml.finish(request)
|
|
94
|
+
# Includes the Okta-style /oauth2/v1/* paths (omniauth-okta and other
|
|
95
|
+
# fixed-path OAuth2 clients) alongside the canonical ones.
|
|
96
|
+
in ["GET", "/login" | "/authorize" | "/v1/authorize" | "/oauth2/v1/authorize"] then @login.form(request)
|
|
97
|
+
in ["GET", "/__select"] then @login.select(request)
|
|
98
|
+
in ["POST", "/oauth2/token"] then @cognito.token(request)
|
|
99
|
+
in ["POST", "/oauth/token"] then @auth0.token(request)
|
|
100
|
+
in ["POST", "/v1/token" | "/oauth2/v1/token"] then @oidc.token(request)
|
|
101
|
+
in ["GET", "/v1/logout"] then @oidc.logout(request)
|
|
102
|
+
in ["GET", "/userinfo" | "/oauth2/v1/userinfo"] then @auth0.userinfo(request)
|
|
103
|
+
in ["POST", "/introspect" | "/oauth2/v1/introspect"] then @oidc.introspect(request)
|
|
104
|
+
in ["POST", "/revoke" | "/oauth2/v1/revoke"] then @oidc.revoke(request)
|
|
105
|
+
in ["GET", "/.well-known/openid-configuration"] then @oidc.discovery
|
|
106
|
+
in ["GET", "/jwks" | "/.well-known/jwks.json" | "/oauth2/v1/keys"] then @oidc.jwks
|
|
107
|
+
else nil
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Identizer
|
|
4
|
+
# What an issued code/refresh token stands for: the signed-in identity plus the
|
|
5
|
+
# authorization-request parameters needed at token time (PKCE, scope, nonce).
|
|
6
|
+
Authorization = Struct.new(:identity, :code_challenge, :code_challenge_method, :scope, :nonce, :client_id,
|
|
7
|
+
:access_token, :refresh_token, keyword_init: true) do
|
|
8
|
+
# RFC 7636 PKCE check. No challenge issued -> nothing to verify.
|
|
9
|
+
def pkce_valid?(verifier)
|
|
10
|
+
return true if code_challenge.to_s.empty?
|
|
11
|
+
|
|
12
|
+
case code_challenge_method
|
|
13
|
+
when "S256"
|
|
14
|
+
digest = Digest::SHA256.digest(verifier.to_s)
|
|
15
|
+
Base64.urlsafe_encode64(digest, padding: false) == code_challenge
|
|
16
|
+
else # "plain" (or unspecified)
|
|
17
|
+
verifier.to_s == code_challenge
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module Identizer
|
|
6
|
+
# `identizer` command: configure from flags/env and boot the standalone server.
|
|
7
|
+
class CLI
|
|
8
|
+
# Seeded on first run so the directory isn't empty and login works immediately.
|
|
9
|
+
DEMO_USER = { mail: "demo@example.com", givenName: "Demo", sn: "User" }.freeze
|
|
10
|
+
|
|
11
|
+
def self.start(argv)
|
|
12
|
+
new(argv).run
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(argv)
|
|
16
|
+
@argv = argv
|
|
17
|
+
@demo = true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run
|
|
21
|
+
config = configure(Identizer.configuration)
|
|
22
|
+
start_ldap(config)
|
|
23
|
+
Server.start(config)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Parse the flags onto a configuration and apply any saved settings, without
|
|
27
|
+
# starting the server. Separated out so it can be exercised in tests.
|
|
28
|
+
def configure(config = Identizer.configuration)
|
|
29
|
+
parser(config).parse!(@argv)
|
|
30
|
+
config.apply_persisted_settings! # web-admin saved password/signing
|
|
31
|
+
config.seed_identities = [DEMO_USER] if @demo && config.seed_identities.empty?
|
|
32
|
+
load_sqlite(config) if config.sqlite_path
|
|
33
|
+
config
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def load_sqlite(config)
|
|
39
|
+
require "identizer/identity_store/sqlite_store"
|
|
40
|
+
config.identity_store = IdentityStore::SqliteStore.new(
|
|
41
|
+
path: config.sqlite_path, base_dn: config.ldap_base_dn, seed: config.seed_identities
|
|
42
|
+
)
|
|
43
|
+
rescue LoadError
|
|
44
|
+
abort "--sqlite needs the sqlite3 gem. Add `gem \"sqlite3\"` to your Gemfile or `gem install sqlite3`."
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def start_ldap(config)
|
|
48
|
+
return unless config.ldap_port || config.ldaps_port
|
|
49
|
+
|
|
50
|
+
require "identizer/ldap"
|
|
51
|
+
Thread.new { Ldap::Server.new(config, port: config.ldap_port).start } if config.ldap_port
|
|
52
|
+
Thread.new { Ldap::Server.new(config, port: config.ldaps_port, tls: true).start } if config.ldaps_port
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def parser(config)
|
|
56
|
+
OptionParser.new do |opts|
|
|
57
|
+
opts.banner = "Usage: identizer [options]"
|
|
58
|
+
|
|
59
|
+
opts.on("--port PORT", Integer, "Listen port (default 9999)") { |value| config.port = value }
|
|
60
|
+
opts.on("--host HOST", "Bind address (default 127.0.0.1)") { |value| config.host = value }
|
|
61
|
+
opts.on("--url-host HOST", "Hostname used in advertised URLs (default localhost)") do |value|
|
|
62
|
+
config.url_host = value
|
|
63
|
+
end
|
|
64
|
+
opts.on("--domain HOST", "Serve under a custom domain (add it to /etc/hosts -> 127.0.0.1)") do |value|
|
|
65
|
+
config.url_host = value
|
|
66
|
+
end
|
|
67
|
+
opts.on("--config-dir DIR", "Where identities + certs are stored") { |value| config.config_dir = value }
|
|
68
|
+
opts.on("--tls-cert PATH", "TLS certificate (PEM)") { |value| config.tls_cert_path = value }
|
|
69
|
+
opts.on("--tls-key PATH", "TLS private key (PEM)") { |value| config.tls_key_path = value }
|
|
70
|
+
opts.on("--password PASS", "Shared sign-in password (default 'password')") do |value|
|
|
71
|
+
config.shared_password = value
|
|
72
|
+
end
|
|
73
|
+
opts.on("--sqlite PATH", "Use a SQLite-backed directory at PATH (needs the sqlite3 gem)") do |value|
|
|
74
|
+
config.sqlite_path = value
|
|
75
|
+
end
|
|
76
|
+
opts.on("--rs256", "Sign id_tokens with RS256 + publish JWKS") { config.signing = :rs256 }
|
|
77
|
+
opts.on("--no-demo", "Don't seed the demo user on first run") { @demo = false }
|
|
78
|
+
opts.on("--quiet", "Don't log requests") { config.request_logging = false }
|
|
79
|
+
opts.on("--ldap-port PORT", Integer, "Also start an LDAP listener on PORT") { |value| config.ldap_port = value }
|
|
80
|
+
opts.on("--ldaps-port PORT", Integer, "Also start an LDAPS (TLS) listener on PORT") do |value|
|
|
81
|
+
config.ldaps_port = value
|
|
82
|
+
end
|
|
83
|
+
opts.on("--ldap-host HOST", "Bind address for the LDAP listener") { |value| config.ldap_host = value }
|
|
84
|
+
opts.on("-v", "--version", "Print version") do
|
|
85
|
+
puts Identizer::VERSION
|
|
86
|
+
exit
|
|
87
|
+
end
|
|
88
|
+
opts.on("-h", "--help", "Show this help") do
|
|
89
|
+
puts opts
|
|
90
|
+
exit
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|