-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
665 lines (625 loc) · 41 KB
/
Copy pathindex.html
File metadata and controls
665 lines (625 loc) · 41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>easyssf · Shared Signals Framework support for Java</title>
<meta name="description" content="easyssf adds an OpenID Shared Signals Framework (SSF) receiver to JVM-based applications, with a Spring Boot starter and a Quarkus extension: it verifies security event tokens, revokes access tokens and ends sessions when the identity provider says so.">
<link rel="icon" href="easyssf-icon.svg" type="image/svg+xml">
<link rel="canonical" href="https://easyssf.org/">
<meta name="google-site-verification" content="a7IMdQaPry9uSsgeSoEvLAKS5D6eS8xMz1yToHgwkIQ">
<meta name="robots" content="index, follow, max-image-preview:large">
<meta name="theme-color" content="#fdf8f1" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#171210" media="(prefers-color-scheme: dark)">
<meta property="og:site_name" content="easyssf">
<meta property="og:title" content="easyssf · Shared Signals Framework support for Java">
<meta property="og:description" content="Add an OpenID Shared Signals Framework receiver to your JVM-based application, Spring Boot and Quarkus included: verified security event tokens, revoked access tokens, ended sessions, tested with the OpenID conformance suite.">
<meta property="og:url" content="https://easyssf.org/">
<meta property="og:type" content="website">
<meta property="og:locale" content="en_US">
<meta property="og:image" content="https://easyssf.org/og-image.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="easyssf: Shared Signals Framework support for Java, as a Spring Boot starter, a Quarkus extension and a plain Java library">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="easyssf · Shared Signals Framework support for Java">
<meta name="twitter:description" content="Add an OpenID Shared Signals Framework receiver to your JVM-based application, Spring Boot and Quarkus included, tested with the OpenID conformance suite.">
<meta name="twitter:image" content="https://easyssf.org/og-image.png">
<link rel="sitemap" type="application/xml" href="sitemap.xml">
<link rel="stylesheet" href="site.css">
<script>/* the chosen theme, before the first paint */try{var t=localStorage.getItem('easyssf.theme');if(t==='light'||t==='dark'){document.documentElement.setAttribute('data-theme',t);}}catch(e){}</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "WebSite",
"@id": "https://easyssf.org/#website",
"url": "https://easyssf.org/",
"name": "easyssf",
"description": "Shared Signals Framework support for Java",
"inLanguage": "en"
},
{
"@type": "SoftwareSourceCode",
"@id": "https://easyssf.org/#software",
"name": "easyssf",
"url": "https://easyssf.org/",
"codeRepository": "https://github.com/easyssf/easyssf",
"programmingLanguage": "Java",
"runtimePlatform": ["Java 21", "Spring Boot 4.1", "Quarkus 3.27"],
"license": "https://www.apache.org/licenses/LICENSE-2.0",
"description": "A Java library that adds an OpenID Shared Signals Framework (SSF) receiver to JVM-based applications, with a Spring Boot starter and a Quarkus extension: it verifies security event tokens, revokes access tokens and ends sessions when the identity provider says so.",
"keywords": ["Shared Signals Framework", "SSF", "CAEP", "RISC", "SCIM Events", "Security Event Token", "OpenID", "Spring Boot", "Quarkus", "Java", "Keycloak"],
"about": [
{"@type": "Thing", "name": "OpenID Shared Signals Framework 1.0", "url": "https://openid.net/specs/openid-sharedsignals-framework-1_0.html"},
{"@type": "Thing", "name": "OpenID CAEP 1.0", "url": "https://openid.net/specs/openid-caep-1_0.html"},
{"@type": "Thing", "name": "OpenID RISC Profile 1.0", "url": "https://openid.net/specs/openid-risc-1_0.html"},
{"@type": "Thing", "name": "RFC 9967 SCIM Events", "url": "https://www.rfc-editor.org/rfc/rfc9967"}
]
}
]
}
</script>
</head>
<body>
<header class="site">
<div class="wrap">
<a class="brand" href="#top" aria-label="easyssf">
<img src="easyssf-icon.svg" alt="" width="26" height="26">
easyssf
</a>
<nav class="top">
<a href="#how">How it works</a>
<a href="#start" class="hide-xs">Getting started</a>
<a href="#specs" class="hide-sm">Specifications</a>
<a href="tools.html">Tools</a>
<a href="#news">News</a>
<a href="https://github.com/easyssf/easyssf">GitHub</a>
<button type="button" class="theme-toggle" data-theme-toggle aria-label="Theme: follows the system" title="Theme: follows the system">
<svg class="icon-auto" viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="7.25" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M10 2.75a7.25 7.25 0 0 1 0 14.5z" fill="currentColor"/></svg>
<svg class="icon-sun" viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="3.75" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M10 1.5v2.5M10 16v2.5M1.5 10H4M16 10h2.5M4 4l1.8 1.8M14.2 14.2 16 16M4 16l1.8-1.8M14.2 5.8 16 4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
<svg class="icon-moon" viewBox="0 0 20 20" aria-hidden="true"><path d="M16.5 12.2A7 7 0 0 1 7.8 3.5a7 7 0 1 0 8.7 8.7z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>
</button>
</nav>
</div>
</header>
<main id="top">
<div class="hero wrap">
<div class="hero-text">
<h1>Shared Signals Framework<br>support for Java</h1>
<p class="lead">easyssf adds an <a href="https://openid.net/specs/openid-sharedsignals-framework-1_0.html">OpenID
Shared Signals Framework</a> receiver to your JVM-based application, so that it reacts when your identity
provider revokes a session, changes a credential or signals a risk. The receiver verifies the security events,
the CAEP events about sessions and credentials, the RISC events about accounts and the SCIM Events about your
users, rejects the access tokens and ends the sessions concerned, and hands everything else to your code.
Spring Boot and Quarkus get a starter and an extension; other JVM applications use the receiver library
directly.</p>
<div class="badges">
<span class="badge">0.2.0 on Maven Central</span>
<span class="badge">Java 21+</span>
<span class="badge">Spring Boot 4.1</span>
<span class="badge">Quarkus 3.27</span>
<span class="badge">Apache License 2.0</span>
<span class="badge">OpenID conformance suite tested</span>
</div>
<div class="cta">
<a class="btn primary" href="#start">Get started</a>
<a class="btn" href="https://github.com/easyssf/easyssf">
<svg viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
Source on GitHub
</a>
</div>
</div>
<!-- decorative: signals travel from the identity provider through the receiver to the application -->
<div class="hero-art" aria-hidden="true">
<svg viewBox="0 0 360 280" focusable="false">
<g class="rings">
<circle class="ring" cx="64" cy="140" r="36"/>
<circle class="ring" cx="64" cy="140" r="36"/>
<circle class="ring" cx="64" cy="140" r="36"/>
</g>
<path class="wire" d="M112,140 L152,140"/>
<path class="wire" d="M248,140 C266,140 266,46 284,46"/>
<path class="wire" d="M248,140 L284,140"/>
<path class="wire" d="M248,140 C266,140 266,234 284,234"/>
<g class="node">
<rect x="16" y="112" width="96" height="56" rx="12"/>
<text x="64" y="136" class="title">Transmitter</text>
<text x="64" y="154" class="small">identity provider</text>
</g>
<g class="node receiver">
<rect x="152" y="100" width="96" height="80" rx="14"/>
<text x="200" y="130" class="title">easyssf</text>
<text x="200" y="149" class="small">verify · dedupe</text>
<text x="200" y="164" class="small">dispatch</text>
</g>
<g class="node">
<rect x="284" y="24" width="64" height="44" rx="10"/>
<text x="316" y="43" class="title">API</text>
<text x="316" y="58" class="small">tokens</text>
</g>
<g class="node">
<rect x="284" y="118" width="64" height="44" rx="10"/>
<text x="316" y="137" class="title">Web app</text>
<text x="316" y="152" class="small">sessions</text>
</g>
<g class="node">
<rect x="284" y="212" width="64" height="44" rx="10"/>
<text x="316" y="231" class="title">Your code</text>
<text x="316" y="246" class="small">handlers</text>
</g>
<circle class="token in" r="6"/>
<circle class="token out out-1" r="5"/>
<circle class="token out out-2" r="5"/>
<circle class="token out out-3" r="5"/>
</svg>
</div>
</div>
<section id="how">
<div class="wrap">
<h2>How it works</h2>
<p class="muted" style="max-width:70ch">A transmitter, usually your identity provider, delivers Security Event Tokens (SETs)
to your application, either by pushing them to an endpoint or by letting the application poll for them.
easyssf verifies each token, skips duplicates and routes the events to the integrations that act on them.</p>
<div class="diagram">
<svg viewBox="0 0 960 320" role="img" aria-labelledby="diag-title">
<title id="diag-title">The transmitter pushes signed Security Event Tokens to your application, or the application polls for them periodically; inside the application the easyssf receiver library verifies them and acts on the resource server, the OIDC client and your handlers</title>
<defs>
<marker id="arrowhead" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8 z" fill="#5e4d44"/></marker>
<marker id="arrowhead-accent" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path class="accent" d="M0,0 L8,4 L0,8 z" fill="#c43f1a"/></marker>
<marker id="arrowhead-small" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto"><path d="M0,0 L6,3 L0,6 z" fill="#5e4d44"/></marker>
</defs>
<!-- transmitter, outside the application -->
<rect class="box" x="14" y="110" width="176" height="100" rx="10"/>
<text class="title" x="102" y="145" text-anchor="middle">Transmitter</text>
<text class="small" x="102" y="166" text-anchor="middle">identity provider, e.g. Keycloak</text>
<text class="small" x="102" y="186" text-anchor="middle">signs SETs with its JWK Set</text>
<!-- your application: everything on the right runs inside it -->
<rect class="box app" x="300" y="20" width="640" height="280" rx="14"/>
<text class="title" x="320" y="48">Your application</text>
<text class="small" x="320" y="66">Spring Boot, Quarkus or any other JVM application; easyssf-receiver is a library inside it</text>
<!-- the receiver library -->
<rect class="box accent" x="330" y="84" width="280" height="196" rx="12"/>
<text class="title" x="470" y="110" text-anchor="middle">easyssf receiver</text>
<rect class="box" x="350" y="124" width="240" height="32" rx="7"/>
<text x="470" y="145" text-anchor="middle">verify the SET · RFC 8417</text>
<rect class="box" x="350" y="164" width="240" height="32" rx="7"/>
<text x="470" y="185" text-anchor="middle">skip duplicates (jti)</text>
<rect class="box" x="350" y="204" width="240" height="32" rx="7"/>
<text x="470" y="225" text-anchor="middle">dispatch to event handlers</text>
<text class="small" x="470" y="256" text-anchor="middle">stream management · metrics</text>
<text class="small" x="470" y="271" text-anchor="middle">state in memory or in your database</text>
<!-- the parts of the application the receiver acts on -->
<rect class="box" x="720" y="84" width="200" height="56" rx="10"/>
<text class="title" x="820" y="107" text-anchor="middle">Resource server</text>
<text class="small" x="820" y="127" text-anchor="middle">rejects revoked access tokens</text>
<rect class="box" x="720" y="154" width="200" height="56" rx="10"/>
<text class="title" x="820" y="177" text-anchor="middle">OIDC client</text>
<text class="small" x="820" y="197" text-anchor="middle">ends the sessions concerned</text>
<rect class="box" x="720" y="224" width="200" height="56" rx="10"/>
<text class="title" x="820" y="247" text-anchor="middle">Your handlers</text>
<text class="small" x="820" y="267" text-anchor="middle">step-up, audit, anything else</text>
<!-- arrows, drawn last so that their heads lie on top of the boxes -->
<!-- push: the transmitter delivers a signed SET to the application's endpoint -->
<path class="arrow accent" marker-end="url(#arrowhead-accent)" d="M190,134 L329,134"/>
<text class="small" x="245" y="112" text-anchor="middle">push · RFC 8935</text>
<!-- poll: the application fetches the SETs periodically -->
<path class="arrow accent" marker-end="url(#arrowhead-accent)" d="M330,188 L191,188"/>
<path class="cycle" marker-end="url(#arrowhead-small)" d="M222,210.2 A6.5,6.5 0 1 1 221.2,201"/>
<text class="small" x="229" y="210">periodically</text>
<text class="small" x="245" y="228" text-anchor="middle">poll · RFC 8936</text>
<!-- a Security Event Token on each way: a signed JWT -->
<g class="set" transform="translate(222,122)">
<rect width="46" height="24" rx="6"/>
<text x="18" y="16.5" text-anchor="middle">SET</text>
<path class="seal" d="M35,8.5 a4,4 0 1 1 0,8 a4,4 0 1 1 0,-8 M35,10.5 v2.5 l1.6,1.2"/>
</g>
<g class="set" transform="translate(222,176)">
<rect width="46" height="24" rx="6"/>
<text x="18" y="16.5" text-anchor="middle">SET</text>
<path class="seal" d="M35,8.5 a4,4 0 1 1 0,8 a4,4 0 1 1 0,-8 M35,10.5 v2.5 l1.6,1.2"/>
</g>
<!-- the receiver acts on the application -->
<path class="arrow" marker-end="url(#arrowhead)" d="M610,182 C670,182 670,112 719,112"/>
<path class="arrow" marker-end="url(#arrowhead)" d="M610,182 L719,182"/>
<path class="arrow" marker-end="url(#arrowhead)" d="M610,182 C670,182 670,252 719,252"/>
</svg>
</div>
<div class="grid">
<div class="card">
<h3>Push or poll<span class="rfc"><a href="https://www.rfc-editor.org/rfc/rfc8935">RFC 8935</a> · <a href="https://www.rfc-editor.org/rfc/rfc8936">RFC 8936</a></span></h3>
<p>A push endpoint on a route of your choice, secured by its own stateless filter chain, or a poller
that fetches and acknowledges SETs from the transmitter.</p>
</div>
<div class="card">
<h3>Every SET verified<span class="rfc"><a href="https://www.rfc-editor.org/rfc/rfc8417">RFC 8417</a></span></h3>
<p>Signature against the transmitter's JWK Set, discovered from its <code>.well-known/ssf-configuration</code>,
plus <code>typ</code>, <code>iss</code>, <code>aud</code>, <code>jti</code>, <code>iat</code> and <code>events</code>.
The transmitter and every endpoint it publishes must use HTTPS.</p>
</div>
<div class="card">
<h3>Resource server integration</h3>
<p>A CAEP <code>session-revoked</code> event makes the application reject the access tokens of that session,
or all tokens of the user issued before the event: built into the Spring Boot starter, a few lines with
<code>quarkus-oidc</code>.</p>
</div>
<div class="card">
<h3>OIDC client integration</h3>
<p><code>session-revoked</code> and <code>credential-change</code> events invalidate the matching local
sessions, by session id, subject or email.</p>
</div>
<div class="card">
<h3>SCIM Events<span class="rfc"><a href="https://www.rfc-editor.org/rfc/rfc9967">RFC 9967</a></span></h3>
<p>The provisioning changes of a SCIM service provider, users created, patched, deactivated or deleted, arrive
as SETs with a <code>scim</code> subject. <code>SsfScimEventHandler</code> hands them to a method per
operation, and a deactivation ends the user's sessions.</p>
</div>
<div class="card">
<h3>Stream management</h3>
<p>Let the application create or update its stream at the transmitter on startup, verify it, and use the
whole stream management API through <code>SsfStreamClient</code>. Several transmitters, several Keycloak
realms say, are each configured by name; the issuer of a SET selects the one that verifies it.</p>
</div>
<div class="card">
<h3>Production details covered</h3>
<p>State in your database when there is one, Micrometer metrics, a health indicator for Actuator or
SmallRye Health, retries with backoff, and the application starts even while the transmitter is down.</p>
</div>
</div>
</div>
</section>
<section id="start">
<div class="wrap">
<h2>Getting started</h2>
<p class="muted" style="max-width:70ch">easyssf comes with a starter for Spring Boot and, through the Quarkiverse extension
<a href="https://github.com/quarkiverse/quarkus-openid-ssf">quarkus-openid-ssf</a>, as an extension for Quarkus.
Both are built on the same receiver library, which other JVM applications use on their own; all three
verify, de-duplicate and dispatch events the same way.</p>
<div class="switch" role="tablist" aria-label="Framework">
<button type="button" role="tab" id="tab-spring-boot" aria-controls="start-spring-boot" aria-selected="true">Spring Boot</button>
<button type="button" role="tab" id="tab-quarkus" aria-controls="start-quarkus" aria-selected="false" tabindex="-1">Quarkus</button>
<button type="button" role="tab" id="tab-plain" aria-controls="start-plain" aria-selected="false" tabindex="-1">Plain Java</button>
</div>
<div id="start-spring-boot" class="framework" role="tabpanel" aria-labelledby="tab-spring-boot">
<p class="muted">Three steps for a Spring Boot application with the servlet stack.</p>
<div class="two steps">
<div class="step">
<h3>Add the starter</h3>
<pre><code class="language-xml"><dependency>
<groupId>org.easyssf</groupId>
<artifactId>easyssf-receiver-spring-boot-starter</artifactId>
<version>0.2.0</version>
</dependency></code></pre>
<p class="muted">From <a href="https://central.sonatype.com/namespace/org.easyssf">Maven Central</a>, where
all modules are listed. Snapshots of <code>main</code> are on the
<a href="https://central.sonatype.com/repository/maven-snapshots/org/easyssf/">Central snapshot repository</a>.</p>
</div>
<div class="step">
<h3>Name the transmitter</h3>
<pre><code class="language-yaml">easyssf:
receiver:
transmitter-issuer: https://idp.example/realms/demo
expected-audience: https://my-app.example
push:
expected-auth-header: Bearer ${SSF_PUSH_SECRET}</code></pre>
</div>
</div>
<div class="step" style="margin-top:20px">
<h3>Create the stream</h3>
<p class="muted">Create a stream with push delivery at the transmitter that points to
<code>https://my-app.example/ssf/push</code> and sends the configured header, or set
<code>easyssf.receiver.stream.management: receiver</code> and let the application create it.
With <code>spring-boot-starter-security-oauth2-resource-server</code> or
<code>spring-boot-starter-security-oauth2-client</code> on the classpath, revoked sessions are handled
from then on without further code.</p>
</div>
<h3 style="margin-top:28px">React to any event</h3>
<pre><code class="language-java">@Component
class StepUpHandler implements SsfEventHandler {
@Override
public void handle(SsfEventContext eventContext) {
if (eventContext.hasEvent("CaepAssuranceLevelChange")) {
SsfSubject subject = eventContext.subjectFor("CaepAssuranceLevelChange");
Map<String, Object> event = eventContext.eventFor("CaepAssuranceLevelChange");
// subject.subject(), subject.sessionId(), subject.email() ...
}
}
}</code></pre>
<p class="muted" style="margin-top:12px">Handlers run before the SET is acknowledged. If one throws, the
transmitter is expected to deliver the SET again.</p>
<h3 style="margin-top:28px">Example applications</h3>
<p class="muted">Complete Spring Boot applications with a Keycloak setup, in the
<a href="https://github.com/easyssf/easyssf/tree/main/easyssf-receiver-spring-boot-examples">examples module</a>:</p>
<ul class="examples">
<li><a href="https://github.com/easyssf/easyssf/tree/main/easyssf-receiver-spring-boot-examples/example-resource-server">OAuth2 resource server</a>: rejects the access tokens of revoked sessions.</li>
<li><a href="https://github.com/easyssf/easyssf/tree/main/easyssf-receiver-spring-boot-examples/example-oidc-client">OIDC relying party</a>: ends the local sessions the events concern.</li>
<li><a href="https://github.com/easyssf/easyssf/tree/main/easyssf-receiver-spring-boot-examples/example-scim-provisioning">SCIM provisioning</a>: mirrors SCIM users into a local directory from SCIM Events.</li>
</ul>
</div>
<div id="start-quarkus" class="framework" role="tabpanel" aria-labelledby="tab-quarkus" hidden>
<p class="muted">Three steps for a Quarkus application.</p>
<div class="two steps">
<div class="step">
<h3>Add the extension</h3>
<pre><code class="language-xml"><dependency>
<groupId>io.quarkiverse.openid-ssf</groupId>
<artifactId>quarkus-openid-ssf-receiver</artifactId>
<version>0.3.0</version>
</dependency></code></pre>
<p class="muted">Or <code>quarkus ext add io.quarkiverse.openid-ssf:quarkus-openid-ssf-receiver</code>.
From <a href="https://central.sonatype.com/namespace/io.quarkiverse.openid-ssf">Maven Central</a>;
<a href="https://github.com/quarkiverse/quarkus-openid-ssf/releases/tag/0.3.0">0.3.0</a> is built on
easyssf 0.2.0 and handles SCIM Events like the Spring Boot starter.</p>
</div>
<div class="step">
<h3>Name the transmitter</h3>
<pre><code class="language-properties">quarkus.openid-ssf.receiver.transmitter-issuer=https://idp.example/realms/demo
quarkus.openid-ssf.receiver.events-requested=CaepSessionRevoked,CaepCredentialChange
quarkus.openid-ssf.receiver.push.delivery-endpoint-url=https://my-app.example/ssf/push
quarkus.openid-ssf.receiver.push.expected-auth-header=Bearer ${SSF_PUSH_SECRET}</code></pre>
</div>
</div>
<div class="step" style="margin-top:20px">
<h3>Create the stream</h3>
<p class="muted">By default the application creates or updates its stream at the transmitter on startup
and tells the transmitter to send the configured header. It authenticates at the stream management API
with a client of the transmitter:</p>
<pre><code class="language-properties">quarkus.openid-ssf.receiver.oauth2.token-endpoint=https://idp.example/realms/demo/protocol/openid-connect/token
quarkus.openid-ssf.receiver.oauth2.client-id=my-app
quarkus.openid-ssf.receiver.oauth2.client-secret=${SSF_CLIENT_SECRET}</code></pre>
<p class="muted" style="margin-top:12px">Or set <code>quarkus.openid-ssf.receiver.stream-management=TRANSMITTER</code>
and the <code>stream-id</code> of a stream an operator created. With <code>quarkus-oidc</code> in the same
application, set <code>quarkus.http.auth.proactive=false</code>, so that the header the transmitter sends to
the push endpoint is not taken for an access token. Rejecting revoked access tokens and ending revoked sessions
take a few lines on top of <code>SsfTokenRevocationEventHandler</code>, a <code>SecurityIdentityAugmentor</code>
in a resource server and a <code>TokenStateManager</code> in a web application; the
<a href="https://github.com/quarkiverse/quarkus-openid-ssf/tree/main/receiver/examples">examples</a> show both.</p>
</div>
<h3 style="margin-top:28px">React to any event</h3>
<pre><code class="language-java">@ApplicationScoped
public class StepUpHandler implements SsfEventHandler {
@Override
public void handle(SsfEventContext eventContext) {
if (eventContext.hasEvent("CaepAssuranceLevelChange")) {
SsfSubject subject = eventContext.subjectFor("CaepAssuranceLevelChange");
Map<String, Object> event = eventContext.eventFor("CaepAssuranceLevelChange");
// subject.subject(), subject.sessionId(), subject.email() ...
}
}
}</code></pre>
<p class="muted" style="margin-top:12px">Every <code>SsfEventHandler</code> bean is invoked for every verified SET,
before the transmitter gets its answer. If one throws, the transmitter is expected to deliver the SET again.</p>
<h3 style="margin-top:28px">Example applications</h3>
<p class="muted">Complete Quarkus applications with a Keycloak setup, in the
<a href="https://github.com/quarkiverse/quarkus-openid-ssf/tree/main/receiver/examples">examples of quarkus-openid-ssf</a>:</p>
<ul class="examples">
<li><a href="https://github.com/quarkiverse/quarkus-openid-ssf/tree/main/receiver/examples/example-resource-server">OAuth2 resource server</a>: rejects the access tokens of revoked sessions.</li>
<li><a href="https://github.com/quarkiverse/quarkus-openid-ssf/tree/main/receiver/examples/example-oidc-client">OIDC relying party</a>: ends the local sessions the events concern.</li>
<li><a href="https://github.com/quarkiverse/quarkus-openid-ssf/tree/main/receiver/examples/example-scim-provisioning">SCIM provisioning</a>: mirrors SCIM users into a local directory from SCIM Events.</li>
</ul>
</div>
<div id="start-plain" class="framework" role="tabpanel" aria-labelledby="tab-plain" hidden>
<p class="muted">For any other framework, or none: <code>easyssf-receiver</code> has no framework dependencies. Add it,
assemble the parts you need and call them from the endpoint or scheduler of your application. The Spring Boot
starter and the Quarkus extension are two such integrations.</p>
<div class="two steps">
<div class="step">
<h3>Add the library</h3>
<pre><code class="language-xml"><dependency>
<groupId>org.easyssf</groupId>
<artifactId>easyssf-receiver</artifactId>
<version>0.2.0</version>
</dependency></code></pre>
<p class="muted">A Java module that depends on <code>easyssf-core</code>, Nimbus JOSE + JWT and SLF4J only. Add
<code>easyssf-receiver-jdbc</code> for database-backed stores.</p>
</div>
<div class="step">
<h3>Assemble the receiver</h3>
<p class="muted">A metadata resolver and a verifier for the transmitter, a processor that de-duplicates and
dispatches to your handlers, and a push handler or a poller for the delivery method of your stream. The
code below is the whole of it.</p>
</div>
</div>
<div class="step" style="margin-top:20px">
<h3>Wire it into your framework</h3>
<p class="muted">Call the push handler from the endpoint the transmitter posts SETs to and return its status
and body, or start the poller from your scheduler. Create the stream at the transmitter, or let
<code>SsfStreamClient</code> do it.</p>
</div>
<h3 style="margin-top:28px">All of it in one place</h3>
<pre><code class="language-java">SsfHttpClient httpClient = new JdkSsfHttpClient();
String issuer = "https://idp.example/realms/demo";
SsfTransmitterMetadataResolver metadata = new SsfTransmitterMetadataResolver(issuer, null, httpClient);
NimbusSsfSetVerifier verifier = new NimbusSsfSetVerifier(issuer,
() -> metadata.resolve().jwksUri().toString(), httpClient);
verifier.setExpectedAudience("https://my-app.example");
SsfEventHandler handler = (eventContext) -> {
if (eventContext.hasEvent("CaepSessionRevoked")) {
SsfSubject subject = eventContext.subjectFor("CaepSessionRevoked");
// end the session subject.sessionId() of the user subject.subject()
}
};
SsfSetProcessor processor = new SsfSetProcessor(verifier, new InMemorySsfJtiDedupStore(10_000), List.of(handler));
// PUSH: call this from the endpoint the transmitter posts SETs to
SsfPushHandler pushHandler = new SsfPushHandler(processor, "Bearer " + pushSecret);
SsfPushResponse response = pushHandler.handle(authorizationHeader, requestBody);
// POLL: fetch SETs from the transmitter instead
SsfPoller poller = new SsfPoller(httpClient, tokenProvider, () -> pollEndpoint, processor);
poller.start();</code></pre>
<p class="muted" style="margin-top:12px">Handlers run before the SET is acknowledged. If one throws, the push handler
answers with an error and the poller does not acknowledge the SET, so the transmitter delivers it again.</p>
<p class="muted" style="margin-top:12px">There is no example application for plain Java yet; the
<a href="https://github.com/easyssf/easyssf#readme">README</a> walks through the assembly, and the handlers of the
Spring Boot and Quarkus examples are plain Java you can reuse.</p>
</div>
<div class="grid">
<div class="card">
<h3>Subjects as the spec defines them</h3>
<p><code>SsfSubject</code> is the <code>sub_id</code> of the SET: a subject identifier in any RFC 9493 format
(<code>iss_sub</code>, <code>email</code>, <code>opaque</code>, <code>account</code>, <code>phone_number</code>,
<code>did</code>, <code>uri</code>, <code>aliases</code>), the <code>scim</code> format of RFC 9967, or a complex subject with its <code>user</code>,
<code>session</code>, <code>device</code>, <code>tenant</code> and other members. Shortcuts cover the common
cases, nothing is lost.</p>
</div>
<div class="card">
<h3>Your own event type aliases</h3>
<p>The SSF, CAEP and RISC event types have built-in aliases such as <code>CaepSessionRevoked</code>. Register
aliases for vendor specific event types, in configuration or in code, and use them wherever an event type is
named. The URIs stay canonical; an alias can never redefine another.</p>
</div>
<div class="card">
<h3>Test your receiver</h3>
<p><code>easyssf-test</code> brings a transmitter that runs inside your test JVM: it signs SETs, serves metadata
and keys, hands out tokens and emulates the stream and poll endpoints. Push a revocation, assert the effect.
The tests of the Spring Boot starter and of the Quarkus extension use it.</p>
</div>
</div>
<div class="note">easyssf is pre-1.0 and its API may still change. The modules are on
<a href="https://central.sonatype.com/namespace/org.easyssf">Maven Central</a> and the source on
<a href="https://github.com/easyssf/easyssf">GitHub</a>. See the <a href="https://github.com/easyssf/easyssf#readme">README</a> for the
complete configuration reference of the Spring Boot starter, and the
<a href="https://github.com/quarkiverse/quarkus-openid-ssf#readme">README of quarkus-openid-ssf</a> for the one
of the Quarkus extension.</div>
</div>
</section>
<section id="modules">
<div class="wrap">
<h2>Modules</h2>
<p class="muted">Everything is published under the group id <code>org.easyssf</code>. The Quarkus extension is a
Quarkiverse project with its own coordinates.</p>
<div class="table-wrap">
<table>
<thead><tr><th>Module</th><th>What it is</th><th>Depends on</th></tr></thead>
<tbody>
<tr><td><code>easyssf-core</code></td><td>The data structures of SSF shared by receivers and, later, transmitters: SETs, subjects, event types, stream configuration, transmitter metadata, and the typed SCIM Events of RFC 9967.</td><td>nothing</td></tr>
<tr><td><code>easyssf-receiver</code></td><td>The receiver, independent of any framework: SET verification, de-duplication, event handlers, push handling, polling, stream management, token revocation and session termination logic.</td><td>easyssf-core, Nimbus JOSE + JWT, SLF4J</td></tr>
<tr><td><code>easyssf-receiver-jdbc</code></td><td>The database-backed stores of the receiver, processed SETs and revocations, independent of any framework: the SQL, the schema and a small execution interface implemented over a <code>DataSource</code> or a framework's template.</td><td>easyssf-receiver</td></tr>
<tr><td><code>easyssf-receiver-spring-boot-starter</code></td><td>The receiver for Spring Boot 4.1 with Spring Security 7.1 on the servlet stack: configuration properties, auto-configuration, push endpoint, resource server and OIDC client integration.</td><td>easyssf-receiver, Spring Boot</td></tr>
<tr><td><code>easyssf-test</code></td><td>Test support: a transmitter on a loopback port that signs and delivers SETs, serves metadata and keys and emulates the stream and poll endpoints, for the tests of your receiver.</td><td>easyssf-core, Nimbus JOSE + JWT</td></tr>
<tr><td><code>easyssf-receiver-spring-boot-examples</code></td><td>An example resource server and OIDC client with a Keycloak setup, and a SCIM provisioning example that mirrors users from SCIM Events.</td><td></td></tr>
<tr><td><code>easyssf-test-conformance</code></td><td>Runs the OpenID conformance suite's SSF receiver test plans against a receiver under test, in any framework: the suite via Testcontainers, the scenarios the receiver plays, and the plan tests to extend.</td><td>easyssf-receiver, Testcontainers, JUnit</td></tr>
<tr><td><code>easyssf-receiver-spring-boot-conformance-tests</code></td><td>The Spring Boot receiver under test and the four plan tests for it.</td><td></td></tr>
<tr><td><a href="https://github.com/quarkiverse/quarkus-openid-ssf"><code>quarkus-openid-ssf-receiver</code></a></td><td>The receiver for Quarkus 3.27, group id <code>io.quarkiverse.openid-ssf</code>: configuration, CDI wiring, the Vert.x push route, the poll scheduler, token providers, Micrometer, SmallRye Health, JDBC, Dev UI and native image support. Its examples and conformance tests live in the same repository.</td><td>easyssf-receiver, easyssf-receiver-jdbc, Quarkus</td></tr>
</tbody>
</table>
</div>
</div>
</section>
<section id="interop">
<div class="wrap">
<h2>Interoperability</h2>
<div class="grid">
<div class="card">
<h3>Keycloak</h3>
<p>Tested with the SSF transmitter of Keycloak 26.8 (<code>--features=ssf</code>). The examples ship a
pre-configured realm, and the README documents Keycloak's audience, scopes and single-stream rule.</p>
</div>
<div class="card">
<h3>caep.dev</h3>
<p>Point the receiver at <a href="https://caep.dev">caep.dev</a>, SGNL's free hosted transmitter, to receive CAEP
events sent from a browser without running an identity provider. See <a href="tools.html#more">Tools</a>.</p>
</div>
<div class="card">
<h3>OpenID conformance suite</h3>
<p>The receiver test plans of the <a href="https://gitlab.com/openid/conformance-suite">OpenID conformance suite</a>,
default and CAEP interop profile with push and poll delivery, run against the receiver in an automated test module.</p>
</div>
<div class="card">
<h3>Standards</h3>
<p>Built on the OpenID Shared Signals specifications and the IETF security event RFCs, see
<a href="#specs">Specifications</a> for the complete list and what easyssf takes from each.</p>
</div>
</div>
<p class="muted" style="margin-top:20px">Not included yet: Spring WebFlux applications and long polling.</p>
</div>
</section>
<section id="specs">
<div class="wrap">
<h2>Specifications</h2>
<p class="muted" style="max-width:70ch">What easyssf implements, and where each piece is defined.</p>
<div class="table-wrap">
<table class="specs">
<thead><tr><th>Specification</th><th>Defines</th><th>In easyssf</th></tr></thead>
<tbody>
<tr>
<td><a href="https://openid.net/specs/openid-sharedsignals-framework-1_0.html">OpenID Shared Signals Framework 1.0</a></td>
<td>Transmitters, receivers, streams, transmitter metadata discovery, stream verification</td>
<td>Metadata discovery, stream management and verification, the SSF event types</td>
</tr>
<tr>
<td><a href="https://openid.net/specs/openid-caep-1_0.html">OpenID CAEP 1.0</a></td>
<td>Continuous Access Evaluation Profile: <code>session-revoked</code>, <code>credential-change</code>, <code>assurance-level-change</code> and the other session and credential events</td>
<td>Event type aliases, the resource server and OIDC client integrations</td>
</tr>
<tr>
<td><a href="https://openid.net/specs/openid-caep-interoperability-profile-1_0.html">OpenID CAEP Interoperability Profile 1.0</a></td>
<td>The minimum a CAEP transmitter and receiver must support to work together</td>
<td>The CAEP interop plans of the conformance tests</td>
</tr>
<tr>
<td><a href="https://openid.net/specs/openid-risc-1_0.html">OpenID RISC Profile 1.0</a></td>
<td>Risk Incident Sharing and Coordination: account disabled, purged, credential compromise and the other account events</td>
<td>Event type aliases, for your own handlers</td>
</tr>
<tr>
<td><a href="https://www.rfc-editor.org/rfc/rfc8417">RFC 8417</a></td>
<td>Security Event Token (SET): the JWT profile every event is delivered in</td>
<td>Verification of signature, <code>typ</code>, <code>iss</code>, <code>aud</code>, <code>jti</code>, <code>iat</code> and <code>events</code></td>
</tr>
<tr>
<td><a href="https://www.rfc-editor.org/rfc/rfc8935">RFC 8935</a></td>
<td>Push-based SET delivery over HTTP</td>
<td>The push endpoint and its responses</td>
</tr>
<tr>
<td><a href="https://www.rfc-editor.org/rfc/rfc8936">RFC 8936</a></td>
<td>Poll-based SET delivery over HTTP</td>
<td>The poller, acknowledgements and <code>setErrs</code></td>
</tr>
<tr>
<td><a href="https://www.rfc-editor.org/rfc/rfc9493">RFC 9493</a></td>
<td>Subject identifiers for SETs: <code>account</code>, <code>email</code>, <code>iss_sub</code>, <code>opaque</code>, <code>phone_number</code>, <code>did</code>, <code>uri</code>, <code>aliases</code></td>
<td><code>SsfSubjectIdentifier</code>, every format, and <code>SsfSubject</code> for complex subjects; the matching of events to sessions and users</td>
</tr>
<tr>
<td><a href="https://www.rfc-editor.org/rfc/rfc9967">RFC 9967</a></td>
<td>SCIM Events: the provisioning changes of a SCIM service provider as SETs, the <code>scim</code> subject identifier, full and notice events</td>
<td>Event type aliases, <code>SsfScimSubject</code>, <code>SsfScimEvent</code> and <code>SsfScimEventHandler</code>; session termination for deactivated and deleted users</td>
</tr>
<tr>
<td><a href="https://www.rfc-editor.org/rfc/rfc7515">RFC 7515</a> / <a href="https://www.rfc-editor.org/rfc/rfc7517">RFC 7517</a></td>
<td>JSON Web Signature and JSON Web Key</td>
<td>Signature checks against the transmitter's JWK Set, with Nimbus JOSE + JWT</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<section id="news">
<div class="wrap">
<h2>News</h2>
<p class="muted">Releases and other changes worth knowing about.</p>
<div class="news-list" data-news="news.json" data-limit="3">
<p class="muted">See the <a href="news.html">news page</a>.</p>
</div>
<p class="news-more"><a href="news.html">All news</a></p>
</div>
</section>
</main>
<footer>
<div class="wrap">
<span>easyssf is open source under the Apache License 2.0. Contact: <a href="mailto:oss@easyssf.org">oss@easyssf.org</a></span>
<span>
<a href="https://github.com/easyssf/easyssf">GitHub</a> ·
<a href="https://github.com/easyssf/easyssf/issues">Issues</a> ·
<a href="https://github.com/easyssf/easyssf#readme">Documentation</a>
</span>
</div>
</footer>
<script src="site.js"></script>
<script src="news.js"></script>
<script src="start.js"></script>
</body>
</html>