refactor(ext-oidc): replace auth modules with security extensions #17
Generated
+2
-2
@@ -17,8 +17,8 @@
|
|||||||
<file url="file://$PROJECT_DIR$/flash-extensions/flash-ext-jackson/src/main/resources" charset="UTF-8" />
|
<file url="file://$PROJECT_DIR$/flash-extensions/flash-ext-jackson/src/main/resources" charset="UTF-8" />
|
||||||
<file url="file://$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java" charset="UTF-8" />
|
<file url="file://$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/java" charset="UTF-8" />
|
||||||
<file url="file://$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/resources" charset="UTF-8" />
|
<file url="file://$PROJECT_DIR$/flash-extensions/flash-ext-limiter/src/main/resources" charset="UTF-8" />
|
||||||
<file url="file://$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/java" charset="UTF-8" />
|
<file url="file://$PROJECT_DIR$/flash-extensions/flash-ext-auth-oidc/src/main/java" charset="UTF-8" />
|
||||||
<file url="file://$PROJECT_DIR$/flash-extensions/flash-ext-oidc/src/main/resources" charset="UTF-8" />
|
<file url="file://$PROJECT_DIR$/flash-extensions/flash-ext-auth-oidc/src/main/resources" charset="UTF-8" />
|
||||||
<file url="file://$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java" charset="UTF-8" />
|
<file url="file://$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/java" charset="UTF-8" />
|
||||||
<file url="file://$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/resources" charset="UTF-8" />
|
<file url="file://$PROJECT_DIR$/flash-extensions/flash-ext-openapi/src/main/resources" charset="UTF-8" />
|
||||||
<file url="file://$PROJECT_DIR$/flash-extensions/flash-ext-routeviewer/src/main/java" charset="UTF-8" />
|
<file url="file://$PROJECT_DIR$/flash-extensions/flash-ext-routeviewer/src/main/java" charset="UTF-8" />
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ a zero-allocation FSM router, bounded protocol state, and one shared request/res
|
|||||||
| `flash-testing` | JUnit 5 harness — boot an app on an ephemeral port, fake its services, assert on responses |
|
| `flash-testing` | JUnit 5 harness — boot an app on an ephemeral port, fake its services, assert on responses |
|
||||||
| `flash-extensions/flash-ext-jackson` | Jackson JSON integration |
|
| `flash-extensions/flash-ext-jackson` | Jackson JSON integration |
|
||||||
| `flash-extensions/flash-ext-openapi` | OpenAPI 3.0 spec + Swagger UI |
|
| `flash-extensions/flash-ext-openapi` | OpenAPI 3.0 spec + Swagger UI |
|
||||||
| `flash-extensions/flash-ext-oidc` | OIDC Authorization Code + PKCE flow |
|
| `flash-extensions/flash-ext-auth-oidc` | OIDC Authorization Code + PKCE flow |
|
||||||
| `flash-extensions/flash-ext-mcp` | MCP (Model Context Protocol) server — Streamable HTTP, optional OAuth2 via flash-ext-oidc |
|
| `flash-extensions/flash-ext-mcp` | MCP (Model Context Protocol) server — Streamable HTTP, optional OAuth2 via flash-ext-auth-oidc |
|
||||||
| `flash-extensions/flash-ext-view-core` | Minimal shared SSR runtime primitives |
|
| `flash-extensions/flash-ext-view-core` | Minimal shared SSR runtime primitives |
|
||||||
| `flash-extensions/flash-ext-view-jte` | Opinionated jte SSR extension |
|
| `flash-extensions/flash-ext-view-jte` | Opinionated jte SSR extension |
|
||||||
| `flash-extensions/flash-ext-view-thymeleaf` | Opinionated Thymeleaf SSR extension |
|
| `flash-extensions/flash-ext-view-thymeleaf` | Opinionated Thymeleaf SSR extension |
|
||||||
@@ -146,7 +146,7 @@ FlashApp.create(8080)
|
|||||||
See extension-specific READMEs for full details:
|
See extension-specific READMEs for full details:
|
||||||
- [`flash-ext-jackson`](flash-extensions/flash-ext-jackson/README.md)
|
- [`flash-ext-jackson`](flash-extensions/flash-ext-jackson/README.md)
|
||||||
- [`flash-ext-openapi`](flash-extensions/flash-ext-openapi/README.md)
|
- [`flash-ext-openapi`](flash-extensions/flash-ext-openapi/README.md)
|
||||||
- [`flash-ext-oidc`](flash-extensions/flash-ext-oidc/README.md)
|
- [`flash-ext-auth-oidc`](flash-extensions/flash-ext-auth-oidc/README.md)
|
||||||
- [`flash-ext-mcp`](flash-extensions/flash-ext-mcp/docs/README.md)
|
- [`flash-ext-mcp`](flash-extensions/flash-ext-mcp/docs/README.md)
|
||||||
- [`flash-ext-view-jte`](flash-extensions/flash-ext-view-jte/README.md)
|
- [`flash-ext-view-jte`](flash-extensions/flash-ext-view-jte/README.md)
|
||||||
- [`flash-ext-view-thymeleaf`](flash-extensions/flash-ext-view-thymeleaf/README.md)
|
- [`flash-ext-view-thymeleaf`](flash-extensions/flash-ext-view-thymeleaf/README.md)
|
||||||
|
|||||||
+29
@@ -5,6 +5,8 @@ import dev.relism.flash.extension.FlashContext;
|
|||||||
import dev.relism.flash.models.Request;
|
import dev.relism.flash.models.Request;
|
||||||
import dev.relism.flash.models.Response;
|
import dev.relism.flash.models.Response;
|
||||||
import dev.relism.flash.routing.Middleware;
|
import dev.relism.flash.routing.Middleware;
|
||||||
|
import dev.relism.flash.routing.MiddlewareKey;
|
||||||
|
import dev.relism.flash.routing.MiddlewareNode;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -28,6 +30,13 @@ import java.util.Map;
|
|||||||
*/
|
*/
|
||||||
public class AuthMiddleware {
|
public class AuthMiddleware {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boot-time identity of the node annotation-driven authorization mounts under. Public so an
|
||||||
|
* extension that contributes its own middleware can order itself around authentication —
|
||||||
|
* {@code MiddlewareNode.of(...).afterIfPresent(AuthMiddleware.POLICY)}.
|
||||||
|
*/
|
||||||
|
public static final MiddlewareKey POLICY = MiddlewareKey.of("flash.auth.policy");
|
||||||
|
|
||||||
private final AuthConfig config;
|
private final AuthConfig config;
|
||||||
private final CredentialSource source;
|
private final CredentialSource source;
|
||||||
private final String[] roleClaimPathParts;
|
private final String[] roleClaimPathParts;
|
||||||
@@ -40,6 +49,26 @@ public class AuthMiddleware {
|
|||||||
this.scopeClaimPathParts = splitClaimPaths(config.scopeClaimPaths());
|
this.scopeClaimPathParts = splitClaimPaths(config.scopeClaimPaths());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the middleware for {@code source}, publishes it in the context and registers the
|
||||||
|
* annotation processor that mounts {@link Authenticated}, {@link RolesAllowed} and
|
||||||
|
* {@link ScopesAllowed} on scanned handlers.
|
||||||
|
*
|
||||||
|
* <p>Every extension that contributes a {@link CredentialSource} calls this rather than
|
||||||
|
* repeating the wiring — the processor and the {@link #POLICY} key belong to one place.
|
||||||
|
*/
|
||||||
|
public static AuthMiddleware install(FlashContext ctx, AuthConfig config, CredentialSource source) {
|
||||||
|
AuthMiddleware middleware = new AuthMiddleware(config, source);
|
||||||
|
ctx.provide(AuthMiddleware.class, middleware);
|
||||||
|
ctx.addAnnotationProcessor(handlerClass -> {
|
||||||
|
AuthPolicy policy = AuthPolicy.compileFromAnnotations(handlerClass);
|
||||||
|
return policy != null
|
||||||
|
? List.of(MiddlewareNode.of(POLICY, middleware.authorize(policy)))
|
||||||
|
: List.of();
|
||||||
|
});
|
||||||
|
return middleware;
|
||||||
|
}
|
||||||
|
|
||||||
// -- Public API -----------------------------------------------------------
|
// -- Public API -----------------------------------------------------------
|
||||||
|
|
||||||
/** The single configured claim path used by every transport for role checks. */
|
/** The single configured claim path used by every transport for role checks. */
|
||||||
|
|||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package dev.relism.flash.ext.auth;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thread-safe in-memory {@link SessionStore}.
|
||||||
|
*
|
||||||
|
* <p>Sessions are lost on restart and not shared across instances. For
|
||||||
|
* production deployments with multiple nodes or restart-persistence requirements,
|
||||||
|
* supply another implementation to whichever {@link CredentialSource} owns the session.
|
||||||
|
*/
|
||||||
|
public final class InMemorySessionStore implements SessionStore {
|
||||||
|
|
||||||
|
private final ConcurrentHashMap<String, Session> store = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Override public void save(Session s) { store.put(s.id(), s); }
|
||||||
|
@Override public Optional<Session> find(String id) { return Optional.ofNullable(store.get(id)); }
|
||||||
|
@Override public void delete(String id) { store.remove(id); }
|
||||||
|
}
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
package dev.relism.flash.ext.auth;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A signed-in caller's server-side session — saved in a {@link SessionStore} and looked up by a
|
||||||
|
* cookie on every request.
|
||||||
|
*
|
||||||
|
* <p>Immutable: renewing one produces a new instance that replaces the old under the same
|
||||||
|
* {@link #id()}.
|
||||||
|
*
|
||||||
|
* <p>{@link #attributes()} is whatever the {@link CredentialSource} needs to keep alongside the
|
||||||
|
* claims and nothing this module interprets — OpenID Connect stores its access, id and refresh
|
||||||
|
* tokens there so that renewal is its business rather than core's.
|
||||||
|
*/
|
||||||
|
public final class Session {
|
||||||
|
|
||||||
|
/** Renew this far before the real expiry, so a session cannot lapse mid-request. */
|
||||||
|
private static final long EAGER_RENEWAL_SECONDS = 30;
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
private final Map<String, Object> claims;
|
||||||
|
private final Instant expiresAt;
|
||||||
|
private final Map<String, Object> attributes;
|
||||||
|
|
||||||
|
public Session(String id, Map<String, Object> claims, Instant expiresAt,
|
||||||
|
Map<String, Object> attributes) {
|
||||||
|
this.id = id;
|
||||||
|
this.claims = Map.copyOf(claims);
|
||||||
|
this.expiresAt = expiresAt;
|
||||||
|
this.attributes = attributes == null ? Map.of() : Map.copyOf(attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True once the session is within {@value #EAGER_RENEWAL_SECONDS} seconds of expiring. */
|
||||||
|
public boolean isExpired() {
|
||||||
|
return Instant.now().isAfter(expiresAt.minusSeconds(EAGER_RENEWAL_SECONDS));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One attribute, or {@code null} when the source never stored it. */
|
||||||
|
public Object attribute(String key) { return attributes.get(key); }
|
||||||
|
|
||||||
|
/** One attribute as a String, or {@code null}. */
|
||||||
|
public String attributeAsString(String key) {
|
||||||
|
Object v = attributes.get(key);
|
||||||
|
return v != null ? v.toString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String id() { return id; }
|
||||||
|
public Map<String, Object> claims() { return claims; }
|
||||||
|
public Instant expiresAt() { return expiresAt; }
|
||||||
|
public Map<String, Object> attributes() { return attributes; }
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
package dev.relism.flash.ext.auth;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where {@link Session}s live between requests. {@link InMemorySessionStore} is the default;
|
||||||
|
* supply another for Redis, JDBC, or anything that survives a restart or spans instances.
|
||||||
|
*/
|
||||||
|
public interface SessionStore {
|
||||||
|
void save(Session session);
|
||||||
|
Optional<Session> find(String sessionId);
|
||||||
|
void delete(String sessionId);
|
||||||
|
}
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
package dev.relism.flash.ext.auth;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The two things a session has to get right: it reports itself expired early enough that it
|
||||||
|
* cannot lapse midway through a request, and it hands back what a credential source stored on it
|
||||||
|
* without interpreting any of it.
|
||||||
|
*/
|
||||||
|
class SessionTest {
|
||||||
|
|
||||||
|
private static Session at(Instant expiry, Map<String, Object> attributes) {
|
||||||
|
return new Session("s1", Map.of("sub", "u1"), expiry, attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aSessionIsExpiredWellBeforeItsDeadline() {
|
||||||
|
// The eager window is what stops a session from lapsing between the check and the handler.
|
||||||
|
assertFalse(at(Instant.now().plusSeconds(120), Map.of()).isExpired());
|
||||||
|
assertTrue(at(Instant.now().plusSeconds(10), Map.of()).isExpired());
|
||||||
|
assertTrue(at(Instant.now().minusSeconds(1), Map.of()).isExpired());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void attributesAreReturnedUninterpreted() {
|
||||||
|
Session session = at(Instant.now().plusSeconds(60), Map.of("oidc.id_token", "abc", "n", 7));
|
||||||
|
assertEquals("abc", session.attributeAsString("oidc.id_token"));
|
||||||
|
assertEquals("7", session.attributeAsString("n"));
|
||||||
|
assertEquals(7, session.attribute("n"));
|
||||||
|
assertNull(session.attributeAsString("absent"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aSessionWithoutAttributesIsUsableRatherThanNull() {
|
||||||
|
assertNull(at(Instant.now().plusSeconds(60), null).attributeAsString("anything"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void claimsAndAttributesAreCopiedAndImmutable() {
|
||||||
|
Map<String, Object> mutable = new HashMap<>(Map.of("k", "v"));
|
||||||
|
Session session = at(Instant.now().plusSeconds(60), mutable);
|
||||||
|
mutable.put("k", "changed");
|
||||||
|
|
||||||
|
assertEquals("v", session.attributeAsString("k"));
|
||||||
|
assertThrows(UnsupportedOperationException.class, () -> session.attributes().put("x", "y"));
|
||||||
|
assertThrows(UnsupportedOperationException.class, () -> session.claims().put("x", "y"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
# flash-ext-oidc
|
# flash-ext-auth-oidc
|
||||||
|
|
||||||
Full OIDC Authorization Code + PKCE flow for the Flash HTTP server.
|
Full OIDC Authorization Code + PKCE flow for the Flash HTTP server.
|
||||||
Supports Keycloak, Authelia, Auth0, Google, and any RFC 8414-compliant provider.
|
Supports Keycloak, Authelia, Auth0, Google, and any RFC 8414-compliant provider.
|
||||||
@@ -25,7 +25,7 @@ hot-path model (middleware compiled at mount time, no heavy runtime work).
|
|||||||
```xml
|
```xml
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>dev.relism</groupId>
|
<groupId>dev.relism</groupId>
|
||||||
<artifactId>flash-ext-oidc</artifactId>
|
<artifactId>flash-ext-auth-oidc</artifactId>
|
||||||
<version>1.0-SNAPSHOT</version>
|
<version>1.0-SNAPSHOT</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
```
|
```
|
||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
<version>2.1.0-SNAPSHOT</version>
|
<version>2.1.0-SNAPSHOT</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>flash-ext-oidc</artifactId>
|
<artifactId>flash-ext-auth-oidc</artifactId>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
+9
-6
@@ -1,5 +1,8 @@
|
|||||||
package dev.relism.flash.ext.oidc;
|
package dev.relism.flash.ext.oidc;
|
||||||
|
|
||||||
|
import dev.relism.flash.ext.auth.InMemorySessionStore;
|
||||||
|
import dev.relism.flash.ext.auth.SessionStore;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Full OIDC client configuration. Build via
|
* Full OIDC client configuration. Build via
|
||||||
* {@link #builder(String, String, String, String)} or {@link #fromEnv()}.
|
* {@link #builder(String, String, String, String)} or {@link #fromEnv()}.
|
||||||
@@ -50,7 +53,7 @@ public final class OidcConfig {
|
|||||||
private final String scopeClaimPaths;
|
private final String scopeClaimPaths;
|
||||||
private final String algorithm;
|
private final String algorithm;
|
||||||
private final String postLogoutRedirectUri;
|
private final String postLogoutRedirectUri;
|
||||||
private final OidcSessionStore sessionStore;
|
private final SessionStore sessionStore;
|
||||||
private final boolean insecureTls;
|
private final boolean insecureTls;
|
||||||
private final ClientAuthMethod clientAuthMethod;
|
private final ClientAuthMethod clientAuthMethod;
|
||||||
private final String schemeName;
|
private final String schemeName;
|
||||||
@@ -68,7 +71,7 @@ public final class OidcConfig {
|
|||||||
this.algorithm = b.algorithm;
|
this.algorithm = b.algorithm;
|
||||||
this.postLogoutRedirectUri = b.postLogoutRedirectUri;
|
this.postLogoutRedirectUri = b.postLogoutRedirectUri;
|
||||||
this.sessionStore = b.sessionStore != null ? b.sessionStore
|
this.sessionStore = b.sessionStore != null ? b.sessionStore
|
||||||
: new InMemoryOidcSessionStore();
|
: new InMemorySessionStore();
|
||||||
this.insecureTls = b.insecureTls;
|
this.insecureTls = b.insecureTls;
|
||||||
this.clientAuthMethod = b.clientAuthMethod;
|
this.clientAuthMethod = b.clientAuthMethod;
|
||||||
this.schemeName = b.schemeName != null ? b.schemeName : deriveScheme(this.issuer);
|
this.schemeName = b.schemeName != null ? b.schemeName : deriveScheme(this.issuer);
|
||||||
@@ -88,7 +91,7 @@ public final class OidcConfig {
|
|||||||
public String scopeClaimPaths() { return scopeClaimPaths; }
|
public String scopeClaimPaths() { return scopeClaimPaths; }
|
||||||
public String algorithm() { return algorithm; }
|
public String algorithm() { return algorithm; }
|
||||||
public String postLogoutRedirectUri() { return postLogoutRedirectUri; }
|
public String postLogoutRedirectUri() { return postLogoutRedirectUri; }
|
||||||
public OidcSessionStore sessionStore() { return sessionStore; }
|
public SessionStore sessionStore() { return sessionStore; }
|
||||||
/** If {@code true}, TLS certificate validation is skipped. <b>Never use in production.</b> */
|
/** If {@code true}, TLS certificate validation is skipped. <b>Never use in production.</b> */
|
||||||
public boolean insecureTls() { return insecureTls; }
|
public boolean insecureTls() { return insecureTls; }
|
||||||
public ClientAuthMethod clientAuthMethod() { return clientAuthMethod; }
|
public ClientAuthMethod clientAuthMethod() { return clientAuthMethod; }
|
||||||
@@ -190,7 +193,7 @@ public final class OidcConfig {
|
|||||||
private String scopeClaimPaths = "scope,scp";
|
private String scopeClaimPaths = "scope,scp";
|
||||||
private String algorithm = "RS256";
|
private String algorithm = "RS256";
|
||||||
private String postLogoutRedirectUri = "/";
|
private String postLogoutRedirectUri = "/";
|
||||||
private OidcSessionStore sessionStore;
|
private SessionStore sessionStore;
|
||||||
private boolean insecureTls = false;
|
private boolean insecureTls = false;
|
||||||
private ClientAuthMethod clientAuthMethod = ClientAuthMethod.POST;
|
private ClientAuthMethod clientAuthMethod = ClientAuthMethod.POST;
|
||||||
private String schemeName = null;
|
private String schemeName = null;
|
||||||
@@ -218,8 +221,8 @@ public final class OidcConfig {
|
|||||||
public Builder algorithm(String algorithm) { this.algorithm = algorithm; return this; }
|
public Builder algorithm(String algorithm) { this.algorithm = algorithm; return this; }
|
||||||
/** Where to redirect after logout (default: {@code /}). */
|
/** Where to redirect after logout (default: {@code /}). */
|
||||||
public Builder postLogoutRedirectUri(String uri) { this.postLogoutRedirectUri = uri; return this; }
|
public Builder postLogoutRedirectUri(String uri) { this.postLogoutRedirectUri = uri; return this; }
|
||||||
/** Custom session store (default: {@link InMemoryOidcSessionStore}). */
|
/** Custom session store (default: {@link InMemorySessionStore}). */
|
||||||
public Builder sessionStore(OidcSessionStore store) { this.sessionStore = store; return this; }
|
public Builder sessionStore(SessionStore store) { this.sessionStore = store; return this; }
|
||||||
/**
|
/**
|
||||||
* Disables TLS certificate verification for all HTTP calls made by this extension.
|
* Disables TLS certificate verification for all HTTP calls made by this extension.
|
||||||
* <b>Only use in development with self-signed certificates — never in production.</b>
|
* <b>Only use in development with self-signed certificates — never in production.</b>
|
||||||
+37
-20
@@ -2,6 +2,7 @@ package dev.relism.flash.ext.oidc;
|
|||||||
|
|
||||||
import dev.relism.flash.exceptions.HttpException;
|
import dev.relism.flash.exceptions.HttpException;
|
||||||
import dev.relism.flash.ext.auth.CredentialSource;
|
import dev.relism.flash.ext.auth.CredentialSource;
|
||||||
|
import dev.relism.flash.ext.auth.Session;
|
||||||
import dev.relism.flash.models.Response;
|
import dev.relism.flash.models.Response;
|
||||||
import dev.relism.flash.models.Request;
|
import dev.relism.flash.models.Request;
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ import java.util.Optional;
|
|||||||
* <p>Resolution order on each request:
|
* <p>Resolution order on each request:
|
||||||
* <ol>
|
* <ol>
|
||||||
* <li>{@code Authorization: Bearer ...} header — validated via JWKS ({@link JwtValidator}).</li>
|
* <li>{@code Authorization: Bearer ...} header — validated via JWKS ({@link JwtValidator}).</li>
|
||||||
* <li>{@code oidc_session} cookie — looked up in {@link OidcSessionStore}; transparently
|
* <li>{@code oidc_session} cookie — looked up in {@link dev.relism.flash.ext.auth.SessionStore}; transparently
|
||||||
* refreshed if the access token is expired.</li>
|
* refreshed if the access token is expired.</li>
|
||||||
* <li>Browser clients (no {@code Accept: application/json}) → redirect to
|
* <li>Browser clients (no {@code Accept: application/json}) → redirect to
|
||||||
* {@code {routePrefix}/login?redirect={path}}.</li>
|
* {@code {routePrefix}/login?redirect={path}}.</li>
|
||||||
@@ -31,6 +32,24 @@ public final class OidcCredentialSource implements CredentialSource {
|
|||||||
|
|
||||||
private static final String BEARER = "Bearer";
|
private static final String BEARER = "Bearer";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keys this source stores its OAuth2 tokens under in {@link Session#attributes()}. Core keeps
|
||||||
|
* the session; the tokens inside it are nobody else's business.
|
||||||
|
*/
|
||||||
|
static final String ACCESS_TOKEN = "oidc.access_token";
|
||||||
|
static final String ID_TOKEN = "oidc.id_token";
|
||||||
|
static final String REFRESH_TOKEN = "oidc.refresh_token";
|
||||||
|
|
||||||
|
/** The one place an OIDC session is built, so its attribute keys stay in one place too. */
|
||||||
|
static Session newSession(String id, String accessToken, String idToken, String refreshToken,
|
||||||
|
Instant expiresAt, Map<String, Object> claims) {
|
||||||
|
Map<String, Object> attributes = new HashMap<>(3);
|
||||||
|
if (accessToken != null) attributes.put(ACCESS_TOKEN, accessToken);
|
||||||
|
if (idToken != null) attributes.put(ID_TOKEN, idToken);
|
||||||
|
if (refreshToken != null) attributes.put(REFRESH_TOKEN, refreshToken);
|
||||||
|
return new Session(id, claims, expiresAt, attributes);
|
||||||
|
}
|
||||||
|
|
||||||
private final JwtValidator validator;
|
private final JwtValidator validator;
|
||||||
private final OidcConfig config;
|
private final OidcConfig config;
|
||||||
private final OidcProviderMetadata meta;
|
private final OidcProviderMetadata meta;
|
||||||
@@ -108,14 +127,14 @@ public final class OidcCredentialSource implements CredentialSource {
|
|||||||
|
|
||||||
String sessionId = cookieValue(req, "oidc_session");
|
String sessionId = cookieValue(req, "oidc_session");
|
||||||
if (sessionId != null) {
|
if (sessionId != null) {
|
||||||
Optional<OidcSession> found = config.sessionStore().find(sessionId);
|
Optional<Session> found = config.sessionStore().find(sessionId);
|
||||||
if (found.isPresent()) {
|
if (found.isPresent()) {
|
||||||
OidcSession session = found.get();
|
Session session = found.get();
|
||||||
if (!session.isAccessTokenExpired())
|
if (!session.isExpired())
|
||||||
return session.claims();
|
return session.claims();
|
||||||
if (session.refreshToken() != null) {
|
if (session.attributeAsString(REFRESH_TOKEN) != null) {
|
||||||
try {
|
try {
|
||||||
OidcSession refreshed = doRefresh(session);
|
Session refreshed = doRefresh(session);
|
||||||
config.sessionStore().save(refreshed);
|
config.sessionStore().save(refreshed);
|
||||||
return refreshed.claims();
|
return refreshed.claims();
|
||||||
} catch (Exception ignored) { }
|
} catch (Exception ignored) { }
|
||||||
@@ -149,17 +168,17 @@ public final class OidcCredentialSource implements CredentialSource {
|
|||||||
// 2. Session cookie
|
// 2. Session cookie
|
||||||
String sessionId = cookieValue(req, "oidc_session");
|
String sessionId = cookieValue(req, "oidc_session");
|
||||||
if (sessionId != null) {
|
if (sessionId != null) {
|
||||||
Optional<OidcSession> found = config.sessionStore().find(sessionId);
|
Optional<Session> found = config.sessionStore().find(sessionId);
|
||||||
if (found.isPresent()) {
|
if (found.isPresent()) {
|
||||||
OidcSession session = found.get();
|
Session session = found.get();
|
||||||
|
|
||||||
if (!session.isAccessTokenExpired())
|
if (!session.isExpired())
|
||||||
return session.claims();
|
return session.claims();
|
||||||
|
|
||||||
// Access token expired — try silent refresh
|
// Access token expired — try silent refresh
|
||||||
if (session.refreshToken() != null) {
|
if (session.attributeAsString(REFRESH_TOKEN) != null) {
|
||||||
try {
|
try {
|
||||||
OidcSession refreshed = doRefresh(session);
|
Session refreshed = doRefresh(session);
|
||||||
config.sessionStore().save(refreshed);
|
config.sessionStore().save(refreshed);
|
||||||
return refreshed.claims();
|
return refreshed.claims();
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
@@ -184,19 +203,17 @@ public final class OidcCredentialSource implements CredentialSource {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private OidcSession doRefresh(OidcSession old) throws Exception {
|
private Session doRefresh(Session old) throws Exception {
|
||||||
OidcTokenResponse tokens = tokenClient.refresh(
|
OidcTokenResponse tokens = tokenClient.refresh(
|
||||||
meta.tokenEndpoint(), old.refreshToken());
|
meta.tokenEndpoint(), old.attributeAsString(REFRESH_TOKEN));
|
||||||
|
|
||||||
Map<String, Object> claims = mergeRefreshedClaims(tokens, old);
|
return newSession(
|
||||||
|
|
||||||
return new OidcSession(
|
|
||||||
old.id(),
|
old.id(),
|
||||||
tokens.accessToken(),
|
tokens.accessToken(),
|
||||||
tokens.idToken() != null ? tokens.idToken() : old.idToken(),
|
tokens.idToken() != null ? tokens.idToken() : old.attributeAsString(ID_TOKEN),
|
||||||
tokens.refreshToken() != null ? tokens.refreshToken() : old.refreshToken(),
|
tokens.refreshToken() != null ? tokens.refreshToken() : old.attributeAsString(REFRESH_TOKEN),
|
||||||
Instant.now().plusSeconds(tokens.expiresIn()),
|
Instant.now().plusSeconds(tokens.expiresIn()),
|
||||||
claims
|
mergeRefreshedClaims(tokens, old)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,7 +297,7 @@ public final class OidcCredentialSource implements CredentialSource {
|
|||||||
return out.toString();
|
return out.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Map<String, Object> mergeRefreshedClaims(OidcTokenResponse tokens, OidcSession old) {
|
private static Map<String, Object> mergeRefreshedClaims(OidcTokenResponse tokens, Session old) {
|
||||||
Map<String, Object> merged = new HashMap<>();
|
Map<String, Object> merged = new HashMap<>();
|
||||||
// Fall back to old claims first, then overlay fresh token claims
|
// Fall back to old claims first, then overlay fresh token claims
|
||||||
merged.putAll(old.claims());
|
merged.putAll(old.claims());
|
||||||
+5
-12
@@ -5,6 +5,7 @@ import dev.relism.flash.ext.openapi.OpenApiContributorRegistry;
|
|||||||
import dev.relism.flash.ext.openapi.OpenApiOperationContribution;
|
import dev.relism.flash.ext.openapi.OpenApiOperationContribution;
|
||||||
import dev.relism.flash.ext.openapi.OpenApiResponseContribution;
|
import dev.relism.flash.ext.openapi.OpenApiResponseContribution;
|
||||||
import dev.relism.flash.ext.auth.AuthConfig;
|
import dev.relism.flash.ext.auth.AuthConfig;
|
||||||
|
import dev.relism.flash.ext.auth.Session;
|
||||||
import dev.relism.flash.ext.auth.AuthMiddleware;
|
import dev.relism.flash.ext.auth.AuthMiddleware;
|
||||||
import dev.relism.flash.ext.auth.AuthPolicy;
|
import dev.relism.flash.ext.auth.AuthPolicy;
|
||||||
import dev.relism.flash.ext.auth.Authenticated;
|
import dev.relism.flash.ext.auth.Authenticated;
|
||||||
@@ -13,8 +14,6 @@ import dev.relism.flash.ext.auth.ScopesAllowed;
|
|||||||
import dev.relism.flash.extension.FlashContext;
|
import dev.relism.flash.extension.FlashContext;
|
||||||
import dev.relism.flash.extension.FlashExtension;
|
import dev.relism.flash.extension.FlashExtension;
|
||||||
import dev.relism.flash.extension.FlashRegistrar;
|
import dev.relism.flash.extension.FlashRegistrar;
|
||||||
import dev.relism.flash.routing.MiddlewareKey;
|
|
||||||
import dev.relism.flash.routing.MiddlewareNode;
|
|
||||||
import dev.relism.flash.models.Request;
|
import dev.relism.flash.models.Request;
|
||||||
|
|
||||||
import javax.net.ssl.SSLContext;
|
import javax.net.ssl.SSLContext;
|
||||||
@@ -63,7 +62,6 @@ import java.util.*;
|
|||||||
* }</pre>
|
* }</pre>
|
||||||
*/
|
*/
|
||||||
public class OidcExtension implements FlashExtension {
|
public class OidcExtension implements FlashExtension {
|
||||||
private static final MiddlewareKey POLICY = MiddlewareKey.of("flash.auth.policy");
|
|
||||||
|
|
||||||
private final OidcConfig config;
|
private final OidcConfig config;
|
||||||
|
|
||||||
@@ -96,19 +94,14 @@ public class OidcExtension implements FlashExtension {
|
|||||||
stateStore = new OidcStateStore();
|
stateStore = new OidcStateStore();
|
||||||
tokenClient = new TokenClient(http, config);
|
tokenClient = new TokenClient(http, config);
|
||||||
source = new OidcCredentialSource(validator, config, meta, tokenClient);
|
source = new OidcCredentialSource(validator, config, meta, tokenClient);
|
||||||
authMw = new AuthMiddleware(AuthConfig.builder()
|
authMw = AuthMiddleware.install(ctx, AuthConfig.builder()
|
||||||
.rolesClaimPath(config.rolesClaimPath())
|
.rolesClaimPath(config.rolesClaimPath())
|
||||||
.scopeClaimPaths(config.scopeClaimPaths())
|
.scopeClaimPaths(config.scopeClaimPaths())
|
||||||
.build(), source);
|
.build(), source);
|
||||||
|
|
||||||
ctx.provide(AuthMiddleware.class, authMw);
|
|
||||||
ctx.provide(OidcCredentialSource.class, source);
|
ctx.provide(OidcCredentialSource.class, source);
|
||||||
ctx.provide(JwtValidator.class, validator);
|
ctx.provide(JwtValidator.class, validator);
|
||||||
|
|
||||||
ctx.addAnnotationProcessor(handlerClass -> {
|
|
||||||
AuthPolicy policy = AuthPolicy.compileFromAnnotations(handlerClass);
|
|
||||||
return policy != null ? List.of(MiddlewareNode.of(POLICY, authMw.authorize(policy))) : List.of();
|
|
||||||
});
|
|
||||||
ctx.onReady(() -> registerRoutes(app, ctx));
|
ctx.onReady(() -> registerRoutes(app, ctx));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +170,7 @@ public class OidcExtension implements FlashExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Object> claims = mergeClaims(tokens);
|
Map<String, Object> claims = mergeClaims(tokens);
|
||||||
OidcSession session = new OidcSession(
|
Session session = OidcCredentialSource.newSession(
|
||||||
UUID.randomUUID().toString(),
|
UUID.randomUUID().toString(),
|
||||||
tokens.accessToken(), tokens.idToken(), tokens.refreshToken(),
|
tokens.accessToken(), tokens.idToken(), tokens.refreshToken(),
|
||||||
Instant.now().plusSeconds(tokens.expiresIn()), claims);
|
Instant.now().plusSeconds(tokens.expiresIn()), claims);
|
||||||
@@ -195,8 +188,8 @@ public class OidcExtension implements FlashExtension {
|
|||||||
String idTokenHint = null;
|
String idTokenHint = null;
|
||||||
|
|
||||||
if (sessionId != null) {
|
if (sessionId != null) {
|
||||||
OidcSession session = config.sessionStore().find(sessionId).orElse(null);
|
Session session = config.sessionStore().find(sessionId).orElse(null);
|
||||||
if (session != null) idTokenHint = session.idToken();
|
if (session != null) idTokenHint = session.attributeAsString(OidcCredentialSource.ID_TOKEN);
|
||||||
config.sessionStore().delete(sessionId);
|
config.sessionStore().delete(sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
`flash-ext-mcp` turns a Flash5 app into an [MCP](https://modelcontextprotocol.io) (Model Context
|
`flash-ext-mcp` turns a Flash5 app into an [MCP](https://modelcontextprotocol.io) (Model Context
|
||||||
Protocol) server: JSON-RPC 2.0 over the Streamable HTTP transport, tools/resources/prompts
|
Protocol) server: JSON-RPC 2.0 over the Streamable HTTP transport, tools/resources/prompts
|
||||||
declared as plain classes and discovered at boot, optional OAuth2 protection built on
|
declared as plain classes and discovered at boot, optional OAuth2 protection built on
|
||||||
`flash-ext-oidc`.
|
`flash-ext-auth-oidc`.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ public class GetWeatherTool extends McpTool {
|
|||||||
`tools-resources-prompts.md`.
|
`tools-resources-prompts.md`.
|
||||||
- **Transport**: Streamable HTTP, `POST`-only, stateless in this revision — see `transport.md`
|
- **Transport**: Streamable HTTP, `POST`-only, stateless in this revision — see `transport.md`
|
||||||
for exactly what that means and why.
|
for exactly what that means and why.
|
||||||
- **Security**: optional, policy-driven OAuth2 via `flash-ext-oidc` — see `security.md`.
|
- **Security**: optional, policy-driven OAuth2 via `flash-ext-auth-oidc` — see `security.md`.
|
||||||
- **JSON**: this extension owns its JSON handling independently of `flash-ext-jackson` — see
|
- **JSON**: this extension owns its JSON handling independently of `flash-ext-jackson` — see
|
||||||
`jackson-interop.md` for why, and how a future opt-in reuse could work.
|
`jackson-interop.md` for why, and how a future opt-in reuse could work.
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ see `tools-resources-prompts.md` — and the fixed `TextContent`/`TextResourceCo
|
|||||||
as a `JsonNode` tree, not as a databound class, for the same reason — a JSON-RPC tool call's
|
as a `JsonNode` tree, not as a databound class, for the same reason — a JSON-RPC tool call's
|
||||||
arguments aren't a DTO with getters/setters, they're a dynamic, per-tool-defined bag of values.
|
arguments aren't a DTO with getters/setters, they're a dynamic, per-tool-defined bag of values.
|
||||||
|
|
||||||
This mirrors how `flash-ext-oidc` already handles its own internal JSON needs (`json-smart` for
|
This mirrors how `flash-ext-auth-oidc` already handles its own internal JSON needs (`json-smart` for
|
||||||
token-endpoint responses) independently of `flash-ext-jackson` — extensions with protocol-level
|
token-endpoint responses) independently of `flash-ext-jackson` — extensions with protocol-level
|
||||||
JSON needs that are shaped by a spec, not by user code, own that JSON handling themselves rather
|
JSON needs that are shaped by a spec, not by user code, own that JSON handling themselves rather
|
||||||
than routing it through the app's general-purpose JSON extension.
|
than routing it through the app's general-purpose JSON extension.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Keycloak cookbook
|
# Keycloak cookbook
|
||||||
|
|
||||||
`security.md` covers the OAuth2 mechanics `McpOidcIntegration` implements against any
|
`security.md` covers the OAuth2 mechanics `McpOidcIntegration` implements against any
|
||||||
`flash-ext-oidc`-compatible provider. This is the Keycloak-specific setup: the exact Admin
|
`flash-ext-auth-oidc`-compatible provider. This is the Keycloak-specific setup: the exact Admin
|
||||||
Console configuration for a working MCP OAuth2 flow with open Dynamic Client Registration
|
Console configuration for a working MCP OAuth2 flow with open Dynamic Client Registration
|
||||||
(DCR) — no pre-registered clients, any MCP client self-registers on first connect.
|
(DCR) — no pre-registered clients, any MCP client self-registers on first connect.
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ Allowed Client Scopes configuration `scopes_supported` needs to actually work.
|
|||||||
|
|
||||||
## `McpSecurity`
|
## `McpSecurity`
|
||||||
|
|
||||||
`McpConfig.security(...)` controls how the MCP endpoint reacts to `flash-ext-oidc` being
|
`McpConfig.security(...)` controls how the MCP endpoint reacts to `flash-ext-auth-oidc` being
|
||||||
installed (`ctx.find(OidcMiddleware.class)`), resolved once at boot in `McpExtension.routes()`:
|
installed (`ctx.find(OidcMiddleware.class)`), resolved once at boot in `McpExtension.routes()`:
|
||||||
|
|
||||||
| Policy | `flash-ext-oidc` installed | `flash-ext-oidc` absent |
|
| Policy | `flash-ext-auth-oidc` installed | `flash-ext-auth-oidc` absent |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `REQUIRED` | protected | **boot fails** (`IllegalStateException`) |
|
| `REQUIRED` | protected | **boot fails** (`IllegalStateException`) |
|
||||||
| `AUTO` (default) | protected | runs unprotected, logs a warning |
|
| `AUTO` (default) | protected | runs unprotected, logs a warning |
|
||||||
@@ -21,18 +21,18 @@ turns "someone forgot to wire up OAuth2" into a startup crash instead of a silen
|
|||||||
endpoint. `AUTO` is meant for local development, where spinning up a real identity provider is
|
endpoint. `AUTO` is meant for local development, where spinning up a real identity provider is
|
||||||
friction you don't want yet.
|
friction you don't want yet.
|
||||||
|
|
||||||
## Why `flash-ext-oidc` is an *optional* Maven dependency, concretely
|
## Why `flash-ext-auth-oidc` is an *optional* Maven dependency, concretely
|
||||||
|
|
||||||
Maven's `<optional>true</optional>` only affects **transitive** propagation: consumers of
|
Maven's `<optional>true</optional>` only affects **transitive** propagation: consumers of
|
||||||
`flash-ext-mcp` don't get `flash-ext-oidc` pulled in automatically unless they add it themselves.
|
`flash-ext-mcp` don't get `flash-ext-auth-oidc` pulled in automatically unless they add it themselves.
|
||||||
Within `flash-ext-mcp` itself, `flash-ext-oidc`'s classes are on the compile/test classpath as
|
Within `flash-ext-mcp` itself, `flash-ext-auth-oidc`'s classes are on the compile/test classpath as
|
||||||
normal — this extension can (and does) reference `OidcMiddleware`/`ClaimsHolder` directly in
|
normal — this extension can (and does) reference `OidcMiddleware`/`ClaimsHolder` directly in
|
||||||
source.
|
source.
|
||||||
|
|
||||||
That reference is isolated in its own class, `McpOidcIntegration`, invoked only from inside a
|
That reference is isolated in its own class, `McpOidcIntegration`, invoked only from inside a
|
||||||
`catch (NoClassDefFoundError)` block. A bare class-literal like `OidcMiddleware.class` (which
|
`catch (NoClassDefFoundError)` block. A bare class-literal like `OidcMiddleware.class` (which
|
||||||
`ctx.find(OidcMiddleware.class)` needs) forces the JVM to resolve that type the moment it's
|
`ctx.find(OidcMiddleware.class)` needs) forces the JVM to resolve that type the moment it's
|
||||||
evaluated — if `flash-ext-oidc` is not on the *runtime* classpath at all (a genuinely
|
evaluated — if `flash-ext-auth-oidc` is not on the *runtime* classpath at all (a genuinely
|
||||||
MCP-only install, no OAuth2 anywhere in the app), the first such reference throws
|
MCP-only install, no OAuth2 anywhere in the app), the first such reference throws
|
||||||
`NoClassDefFoundError`. Keeping that reference inside a separate, lazily-loaded class means
|
`NoClassDefFoundError`. Keeping that reference inside a separate, lazily-loaded class means
|
||||||
`McpExtension` itself loads and works fine standalone; only the attempt to actually use OIDC
|
`McpExtension` itself loads and works fine standalone; only the attempt to actually use OIDC
|
||||||
@@ -45,7 +45,7 @@ When oidc is available and `security() != NONE`, `McpOidcIntegration` (an isolat
|
|||||||
lazily-loaded bridge — see its javadoc) derives everything an MCP OAuth2 resource server needs
|
lazily-loaded bridge — see its javadoc) derives everything an MCP OAuth2 resource server needs
|
||||||
straight from the installed `OidcMiddleware`, with no additional `McpConfig` calls required:
|
straight from the installed `OidcMiddleware`, with no additional `McpConfig` calls required:
|
||||||
|
|
||||||
1. The MCP route is wrapped with `flash-ext-oidc`'s own `OidcMiddleware.protect(resourceMetadataPath)`
|
1. The MCP route is wrapped with `flash-ext-auth-oidc`'s own `OidcMiddleware.protect(resourceMetadataPath)`
|
||||||
— the same Bearer-token/JWKS validation path used everywhere else in Flash5, plus a
|
— the same Bearer-token/JWKS validation path used everywhere else in Flash5, plus a
|
||||||
`resource_metadata` challenge parameter (see below). No JWT parsing or JWKS handling is
|
`resource_metadata` challenge parameter (see below). No JWT parsing or JWKS handling is
|
||||||
reimplemented here.
|
reimplemented here.
|
||||||
@@ -114,7 +114,7 @@ unaffected — this parameter is additive and MCP-specific.
|
|||||||
|
|
||||||
## Per-tool `@RolesAllowed`/`@ScopesAllowed`
|
## Per-tool `@RolesAllowed`/`@ScopesAllowed`
|
||||||
|
|
||||||
`McpTool` subclasses can carry `flash-ext-oidc`'s `@RolesAllowed`/`@ScopesAllowed`:
|
`McpTool` subclasses can carry `flash-ext-auth-oidc`'s `@RolesAllowed`/`@ScopesAllowed`:
|
||||||
|
|
||||||
```java
|
```java
|
||||||
@Tool(name = "delete_route", description = "Delete a route")
|
@Tool(name = "delete_route", description = "Delete a route")
|
||||||
@@ -124,7 +124,7 @@ public class DeleteRouteTool extends McpTool {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
This does **not** reuse `flash-ext-oidc`'s per-route middleware mechanism (`ctx.addAnnotationProcessor`,
|
This does **not** reuse `flash-ext-auth-oidc`'s per-route middleware mechanism (`ctx.addAnnotationProcessor`,
|
||||||
the thing that makes these annotations work on a `RequestHandler`) — it can't: every tool shares
|
the thing that makes these annotations work on a `RequestHandler`) — it can't: every tool shares
|
||||||
one HTTP route (`POST {rootPath}`), already wrapped by whatever `McpSecurity` resolved above, so
|
one HTTP route (`POST {rootPath}`), already wrapped by whatever `McpSecurity` resolved above, so
|
||||||
there is no per-tool route to attach a different middleware chain to. Instead,
|
there is no per-tool route to attach a different middleware chain to. Instead,
|
||||||
@@ -156,7 +156,7 @@ at `app.start()`.
|
|||||||
|
|
||||||
## The `HttpException` safety net
|
## The `HttpException` safety net
|
||||||
|
|
||||||
`flash-ext-oidc`'s middleware throws `HttpException.unauthorized()`/`forbidden()` on auth
|
`flash-ext-auth-oidc`'s middleware throws `HttpException.unauthorized()`/`forbidden()` on auth
|
||||||
failure. Flash5's core does **not** special-case `HttpException` in the default exception
|
failure. Flash5's core does **not** special-case `HttpException` in the default exception
|
||||||
handler — the out-of-the-box `AbstractRouter` default always returns a generic `500`, regardless
|
handler — the out-of-the-box `AbstractRouter` default always returns a generic `500`, regardless
|
||||||
of the thrown exception's embedded status code; only an app that explicitly calls
|
of the thrown exception's embedded status code; only an app that explicitly calls
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>dev.relism</groupId>
|
<groupId>dev.relism</groupId>
|
||||||
<artifactId>flash-ext-oidc</artifactId>
|
<artifactId>flash-ext-auth-oidc</artifactId>
|
||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ import java.util.function.Supplier;
|
|||||||
*
|
*
|
||||||
* <p>{@code check} is a closure, not a raw role/scope list — this is what lets this record (and
|
* <p>{@code check} is a closure, not a raw role/scope list — this is what lets this record (and
|
||||||
* its only caller, {@link McpDispatcher}) stay free of any compile-time reference to a {@code
|
* its only caller, {@link McpDispatcher}) stay free of any compile-time reference to a {@code
|
||||||
* flash-ext-oidc} type, preserving the same classload isolation {@link McpOidcIntegration}'s
|
* flash-ext-auth-oidc} type, preserving the same classload isolation {@link McpOidcIntegration}'s
|
||||||
* javadoc describes for the rest of the OIDC bridge. Only the plain-JDK {@link Supplier}
|
* javadoc describes for the rest of the OIDC bridge. Only the plain-JDK {@link Supplier}
|
||||||
* signature crosses the boundary; the closure itself, built once inside {@code
|
* signature crosses the boundary; the closure itself, built once inside {@code
|
||||||
* McpOidcIntegration}, is the only place that ever touches {@code OidcUser}/{@code ClaimsHolder}.
|
* McpOidcIntegration}, is the only place that ever touches {@code OidcUser}/{@code ClaimsHolder}.
|
||||||
|
|||||||
+2
-2
@@ -91,7 +91,7 @@ public final class McpConfig {
|
|||||||
/**
|
/**
|
||||||
* Canonical URI of this MCP endpoint, used for RFC 8707 audience binding: tokens whose
|
* Canonical URI of this MCP endpoint, used for RFC 8707 audience binding: tokens whose
|
||||||
* {@code aud} claim does not include this value are rejected. Optional — when
|
* {@code aud} claim does not include this value are rejected. Optional — when
|
||||||
* {@code flash-ext-oidc} is installed, this is auto-derived per request from the
|
* {@code flash-ext-auth-oidc} is installed, this is auto-derived per request from the
|
||||||
* forwarded/{@code Host} headers (same resolution {@code OidcExtension} uses for its own
|
* forwarded/{@code Host} headers (same resolution {@code OidcExtension} uses for its own
|
||||||
* redirect URIs) and audience binding is enforced unconditionally. Set this explicitly
|
* redirect URIs) and audience binding is enforced unconditionally. Set this explicitly
|
||||||
* only to override that guess — a reverse proxy that forwards neither
|
* only to override that guess — a reverse proxy that forwards neither
|
||||||
@@ -102,7 +102,7 @@ public final class McpConfig {
|
|||||||
/**
|
/**
|
||||||
* Authorization server issuer URL, published in the RFC 9728 Protected Resource
|
* Authorization server issuer URL, published in the RFC 9728 Protected Resource
|
||||||
* Metadata document at {@code /.well-known/oauth-protected-resource{rootPath}}. Optional
|
* Metadata document at {@code /.well-known/oauth-protected-resource{rootPath}}. Optional
|
||||||
* — when {@code flash-ext-oidc} is installed, this is auto-derived from its configured
|
* — when {@code flash-ext-auth-oidc} is installed, this is auto-derived from its configured
|
||||||
* issuer. Set this explicitly only to override that (e.g. publishing a different issuer
|
* issuer. Set this explicitly only to override that (e.g. publishing a different issuer
|
||||||
* than the one actually validating tokens).
|
* than the one actually validating tokens).
|
||||||
*/
|
*/
|
||||||
|
|||||||
+5
-5
@@ -25,7 +25,7 @@ import java.util.List;
|
|||||||
* .build()))
|
* .build()))
|
||||||
* .start();
|
* .start();
|
||||||
*
|
*
|
||||||
* // With flash-ext-oidc as the OAuth2 resource server — zero extra config: issuer, canonical
|
* // With flash-ext-auth-oidc as the OAuth2 resource server — zero extra config: issuer, canonical
|
||||||
* // resource identifier, RFC 8707 audience binding and RFC 9728 metadata are all derived from
|
* // resource identifier, RFC 8707 audience binding and RFC 9728 metadata are all derived from
|
||||||
* // the installed OidcExtension.
|
* // the installed OidcExtension.
|
||||||
* FlashApp.create(8080)
|
* FlashApp.create(8080)
|
||||||
@@ -83,20 +83,20 @@ public class McpExtension implements FlashExtension {
|
|||||||
try {
|
try {
|
||||||
resolved = McpOidcIntegration.resolve(ctx, config);
|
resolved = McpOidcIntegration.resolve(ctx, config);
|
||||||
} catch (NoClassDefFoundError e) {
|
} catch (NoClassDefFoundError e) {
|
||||||
resolved = null; // flash-ext-oidc not on the classpath at all
|
resolved = null; // flash-ext-auth-oidc not on the classpath at all
|
||||||
}
|
}
|
||||||
if (resolved != null) return resolved;
|
if (resolved != null) return resolved;
|
||||||
|
|
||||||
if (config.security() == McpSecurity.REQUIRED) {
|
if (config.security() == McpSecurity.REQUIRED) {
|
||||||
throw new IllegalStateException(
|
throw new IllegalStateException(
|
||||||
"McpSecurity.REQUIRED but flash-ext-oidc is not installed for MCP server \"" + config.name() +
|
"McpSecurity.REQUIRED but flash-ext-auth-oidc is not installed for MCP server \"" + config.name() +
|
||||||
"\" — install an OidcExtension before this McpExtension, or relax security to " +
|
"\" — install an OidcExtension before this McpExtension, or relax security to " +
|
||||||
"McpSecurity.AUTO/NONE if this server is meant to be public.");
|
"McpSecurity.AUTO/NONE if this server is meant to be public.");
|
||||||
}
|
}
|
||||||
|
|
||||||
log.warn("[flash-ext-mcp] MCP server \"{}\" is running WITHOUT OAuth2 protection — " +
|
log.warn("[flash-ext-mcp] MCP server \"{}\" is running WITHOUT OAuth2 protection — " +
|
||||||
"flash-ext-oidc is not installed and McpSecurity.AUTO degrades to unprotected. " +
|
"flash-ext-auth-oidc is not installed and McpSecurity.AUTO degrades to unprotected. " +
|
||||||
"Install flash-ext-oidc or set McpSecurity.REQUIRED to make this a hard failure instead.",
|
"Install flash-ext-auth-oidc or set McpSecurity.REQUIRED to make this a hard failure instead.",
|
||||||
config.name());
|
config.name());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import java.nio.charset.StandardCharsets;
|
|||||||
*
|
*
|
||||||
* <p>Not wired to {@code flash-ext-jackson} on purpose: the MCP JSON-RPC envelope is internal
|
* <p>Not wired to {@code flash-ext-jackson} on purpose: the MCP JSON-RPC envelope is internal
|
||||||
* protocol plumbing, not a user-facing serialization concern, so this extension owns its
|
* protocol plumbing, not a user-facing serialization concern, so this extension owns its
|
||||||
* mapper independently — same reasoning {@code flash-ext-oidc} applies to its own JSON needs
|
* mapper independently — same reasoning {@code flash-ext-auth-oidc} applies to its own JSON needs
|
||||||
* (see {@code json-smart} there). See {@code docs/jackson-interop.md} for the full rationale
|
* (see {@code json-smart} there). See {@code docs/jackson-interop.md} for the full rationale
|
||||||
* and how a future opt-in reuse of a shared {@code ObjectMapper} could work.
|
* and how a future opt-in reuse of a shared {@code ObjectMapper} could work.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+4
-4
@@ -20,19 +20,19 @@ import java.util.function.Function;
|
|||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lazy, isolated bridge to {@code flash-ext-oidc} and {@code flash-ext-auth-core}.
|
* Lazy, isolated bridge to {@code flash-ext-auth-oidc} and {@code flash-ext-auth-core}.
|
||||||
*
|
*
|
||||||
* <p>References to OIDC types only ever resolve when {@link #resolve}/{@link #compileToolPolicy}
|
* <p>References to OIDC types only ever resolve when {@link #resolve}/{@link #compileToolPolicy}
|
||||||
* are actually invoked — never at {@link McpExtension} class-load time — because they live in
|
* are actually invoked — never at {@link McpExtension} class-load time — because they live in
|
||||||
* this separate nested class. The caller wraps the invocation in {@code catch
|
* this separate nested class. The caller wraps the invocation in {@code catch
|
||||||
* (NoClassDefFoundError)}, exactly like {@code OidcExtension}'s own lazy bridge to {@code
|
* (NoClassDefFoundError)}, exactly like {@code OidcExtension}'s own lazy bridge to {@code
|
||||||
* flash-ext-openapi}. This is what lets {@code flash-ext-mcp} run standalone (MCP-only, no
|
* flash-ext-openapi}. This is what lets {@code flash-ext-mcp} run standalone (MCP-only, no
|
||||||
* OAuth2) when {@code flash-ext-oidc} is not even on the classpath. {@link Resolved}/{@link
|
* OAuth2) when {@code flash-ext-auth-oidc} is not even on the classpath. {@link Resolved}/{@link
|
||||||
* McpAuthPolicy} carry only oidc-free types back out ({@link Middleware}, {@link String}, a
|
* McpAuthPolicy} carry only oidc-free types back out ({@link Middleware}, {@link String}, a
|
||||||
* {@link Function}, a {@link Supplier}) so no other class in this package ever has to reference
|
* {@link Function}, a {@link Supplier}) so no other class in this package ever has to reference
|
||||||
* an OIDC type.
|
* an OIDC type.
|
||||||
*
|
*
|
||||||
* <p>Zero-config by design: when {@code flash-ext-oidc} is installed, everything an MCP OAuth2
|
* <p>Zero-config by design: when {@code flash-ext-auth-oidc} is installed, everything an MCP OAuth2
|
||||||
* resource server needs — issuer, canonical resource identifier, RFC 8707 audience binding, and
|
* resource server needs — issuer, canonical resource identifier, RFC 8707 audience binding, and
|
||||||
* a spec-compliant {@code WWW-Authenticate} challenge (RFC 9728 §5.1) — is derived straight from
|
* a spec-compliant {@code WWW-Authenticate} challenge (RFC 9728 §5.1) — is derived straight from
|
||||||
* the installed {@link OidcCredentialSource}, with no additional {@link McpConfig} calls.
|
* the installed {@link OidcCredentialSource}, with no additional {@link McpConfig} calls.
|
||||||
@@ -123,7 +123,7 @@ final class McpOidcIntegration {
|
|||||||
if (!oidcActive) {
|
if (!oidcActive) {
|
||||||
throw new IllegalStateException(
|
throw new IllegalStateException(
|
||||||
"MCP tool \"" + toolClass.getSimpleName() + "\" declares @Authenticated/@RolesAllowed/" +
|
"MCP tool \"" + toolClass.getSimpleName() + "\" declares @Authenticated/@RolesAllowed/" +
|
||||||
"@ScopesAllowed, but this MCP server has no active OAuth2 protection — flash-ext-oidc " +
|
"@ScopesAllowed, but this MCP server has no active OAuth2 protection — flash-ext-auth-oidc " +
|
||||||
"is not installed for it, or McpSecurity is NONE. These annotations require " +
|
"is not installed for it, or McpSecurity is NONE. These annotations require " +
|
||||||
"McpSecurity.AUTO/REQUIRED with an OidcExtension installed; install one, or remove the " +
|
"McpSecurity.AUTO/REQUIRED with an OidcExtension installed; install one, or remove the " +
|
||||||
"annotation from " + toolClass.getSimpleName() + ".");
|
"annotation from " + toolClass.getSimpleName() + ".");
|
||||||
|
|||||||
+1
-1
@@ -175,7 +175,7 @@ final class McpRegistry {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Isolated the same way {@link McpOidcIntegration#resolve} is — {@code
|
* Isolated the same way {@link McpOidcIntegration#resolve} is — {@code
|
||||||
* NoClassDefFoundError} here means {@code flash-ext-oidc} genuinely isn't on the runtime
|
* NoClassDefFoundError} here means {@code flash-ext-auth-oidc} genuinely isn't on the runtime
|
||||||
* classpath, in which case a tool couldn't have been compiled against
|
* classpath, in which case a tool couldn't have been compiled against
|
||||||
* {@code @RolesAllowed}/{@code @ScopesAllowed} in the first place, so there's nothing to
|
* {@code @RolesAllowed}/{@code @ScopesAllowed} in the first place, so there's nothing to
|
||||||
* check (and nothing lost: {@code oidcActive} is only ever {@code true} once {@link
|
* check (and nothing lost: {@code oidcActive} is only ever {@code true} once {@link
|
||||||
|
|||||||
+4
-4
@@ -2,16 +2,16 @@ package dev.relism.flash.ext.mcp;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* OAuth2 requirement policy for the MCP endpoint, resolved against whether
|
* OAuth2 requirement policy for the MCP endpoint, resolved against whether
|
||||||
* {@code flash-ext-oidc} is installed ({@code ctx.find(OidcMiddleware.class)}).
|
* {@code flash-ext-auth-oidc} is installed ({@code ctx.find(OidcMiddleware.class)}).
|
||||||
*/
|
*/
|
||||||
public enum McpSecurity {
|
public enum McpSecurity {
|
||||||
|
|
||||||
/** Fail fast at boot if {@code flash-ext-oidc} is not installed — never expose an unprotected MCP endpoint. */
|
/** Fail fast at boot if {@code flash-ext-auth-oidc} is not installed — never expose an unprotected MCP endpoint. */
|
||||||
REQUIRED,
|
REQUIRED,
|
||||||
|
|
||||||
/** Protect the endpoint if {@code flash-ext-oidc} is installed; otherwise run unprotected and log a warning. */
|
/** Protect the endpoint if {@code flash-ext-auth-oidc} is installed; otherwise run unprotected and log a warning. */
|
||||||
AUTO,
|
AUTO,
|
||||||
|
|
||||||
/** Never protect the endpoint, even if {@code flash-ext-oidc} is installed elsewhere in the app. */
|
/** Never protect the endpoint, even if {@code flash-ext-auth-oidc} is installed elsewhere in the app. */
|
||||||
NONE
|
NONE
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -38,7 +38,7 @@ final class McpTransportGuards {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Safety net around the whole MCP route: translates {@link HttpException} (thrown by
|
* Safety net around the whole MCP route: translates {@link HttpException} (thrown by
|
||||||
* {@link #originGuard} or by {@code flash-ext-oidc}'s middleware) into a proper HTTP status
|
* {@link #originGuard} or by {@code flash-ext-auth-oidc}'s middleware) into a proper HTTP status
|
||||||
* directly, instead of relying on the app's global exception handler — which defaults to a
|
* directly, instead of relying on the app's global exception handler — which defaults to a
|
||||||
* generic 500 for every exception type unless the app owner overrides it (see
|
* generic 500 for every exception type unless the app owner overrides it (see
|
||||||
* {@code AbstractRouter}'s default {@code exceptionHandler}). Keeps the MCP endpoint
|
* {@code AbstractRouter}'s default {@code exceptionHandler}). Keeps the MCP endpoint
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ import java.util.UUID;
|
|||||||
/**
|
/**
|
||||||
* Minimal, self-contained fake OIDC provider for tests: real discovery document, real JWKS
|
* Minimal, self-contained fake OIDC provider for tests: real discovery document, real JWKS
|
||||||
* endpoint, real RS256-signed tokens — no network dependency beyond localhost, no mocking
|
* endpoint, real RS256-signed tokens — no network dependency beyond localhost, no mocking
|
||||||
* framework. Exercises {@code flash-ext-oidc}'s actual discovery + JWKS + JWT validation path.
|
* framework. Exercises {@code flash-ext-auth-oidc}'s actual discovery + JWKS + JWT validation path.
|
||||||
*/
|
*/
|
||||||
final class FakeOidcProvider implements AutoCloseable {
|
final class FakeOidcProvider implements AutoCloseable {
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exercises the actual OAuth2 resolution rules against a real {@code flash-ext-oidc}
|
* Exercises the actual OAuth2 resolution rules against a real {@code flash-ext-auth-oidc}
|
||||||
* installation backed by {@link FakeOidcProvider} — real discovery, real JWKS, real RS256
|
* installation backed by {@link FakeOidcProvider} — real discovery, real JWKS, real RS256
|
||||||
* tokens — plus the fail-fast/degrade behavior when oidc is absent.
|
* tokens — plus the fail-fast/degrade behavior when oidc is absent.
|
||||||
*
|
*
|
||||||
|
|||||||
-20
@@ -1,20 +0,0 @@
|
|||||||
package dev.relism.flash.ext.oidc;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Thread-safe in-memory {@link OidcSessionStore}.
|
|
||||||
*
|
|
||||||
* <p>Sessions are lost on restart and not shared across instances. For
|
|
||||||
* production deployments with multiple nodes or restart-persistence requirements,
|
|
||||||
* supply a custom implementation via {@link OidcConfig.Builder#sessionStore}.
|
|
||||||
*/
|
|
||||||
public final class InMemoryOidcSessionStore implements OidcSessionStore {
|
|
||||||
|
|
||||||
private final ConcurrentHashMap<String, OidcSession> store = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
@Override public void save(OidcSession s) { store.put(s.id(), s); }
|
|
||||||
@Override public Optional<OidcSession> find(String id) { return Optional.ofNullable(store.get(id)); }
|
|
||||||
@Override public void delete(String id) { store.remove(id); }
|
|
||||||
}
|
|
||||||
-47
@@ -1,47 +0,0 @@
|
|||||||
package dev.relism.flash.ext.oidc;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An authenticated user's OIDC session — persisted in {@link OidcSessionStore} and
|
|
||||||
* looked up via the {@code oidc_session} cookie on every request.
|
|
||||||
*
|
|
||||||
* <p>Sessions are immutable; a refreshed access token produces a new instance
|
|
||||||
* that replaces the old one in the store (same {@link #id()}).
|
|
||||||
*/
|
|
||||||
public final class OidcSession {
|
|
||||||
|
|
||||||
private final String id;
|
|
||||||
private final String accessToken;
|
|
||||||
private final String idToken;
|
|
||||||
private final String refreshToken; // may be null
|
|
||||||
private final Instant accessTokenExpiresAt;
|
|
||||||
private final Map<String, Object> claims; // decoded from id_token
|
|
||||||
|
|
||||||
public OidcSession(String id, String accessToken, String idToken,
|
|
||||||
String refreshToken, Instant accessTokenExpiresAt,
|
|
||||||
Map<String, Object> claims) {
|
|
||||||
this.id = id;
|
|
||||||
this.accessToken = accessToken;
|
|
||||||
this.idToken = idToken;
|
|
||||||
this.refreshToken = refreshToken;
|
|
||||||
this.accessTokenExpiresAt = accessTokenExpiresAt;
|
|
||||||
this.claims = Map.copyOf(claims);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns {@code true} if the access token has expired or will expire within
|
|
||||||
* the next 30 seconds (eager refresh to avoid mid-request expiry).
|
|
||||||
*/
|
|
||||||
public boolean isAccessTokenExpired() {
|
|
||||||
return Instant.now().isAfter(accessTokenExpiresAt.minusSeconds(30));
|
|
||||||
}
|
|
||||||
|
|
||||||
public String id() { return id; }
|
|
||||||
public String accessToken() { return accessToken; }
|
|
||||||
public String idToken() { return idToken; }
|
|
||||||
public String refreshToken() { return refreshToken; }
|
|
||||||
public Instant accessTokenExpiresAt() { return accessTokenExpiresAt; }
|
|
||||||
public Map<String, Object> claims() { return claims; }
|
|
||||||
}
|
|
||||||
-14
@@ -1,14 +0,0 @@
|
|||||||
package dev.relism.flash.ext.oidc;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Backing store for {@link OidcSession} objects. The default implementation is
|
|
||||||
* {@link InMemoryOidcSessionStore}; supply a custom one via
|
|
||||||
* {@link OidcConfig.Builder#sessionStore(OidcSessionStore)} for Redis, JDBC, etc.
|
|
||||||
*/
|
|
||||||
public interface OidcSessionStore {
|
|
||||||
void save(OidcSession session);
|
|
||||||
Optional<OidcSession> find(String sessionId);
|
|
||||||
void delete(String sessionId);
|
|
||||||
}
|
|
||||||
@@ -123,7 +123,7 @@ Merge policy:
|
|||||||
|
|
||||||
## OIDC interop
|
## OIDC interop
|
||||||
|
|
||||||
When `flash-ext-oidc` is installed, OpenAPI integrates automatically:
|
When `flash-ext-auth-oidc` is installed, OpenAPI integrates automatically:
|
||||||
|
|
||||||
- security scheme under `components.securitySchemes`
|
- security scheme under `components.securitySchemes`
|
||||||
- per-operation `security`
|
- per-operation `security`
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<module>flash-ext-jackson</module>
|
<module>flash-ext-jackson</module>
|
||||||
<module>flash-ext-openapi</module>
|
<module>flash-ext-openapi</module>
|
||||||
<module>flash-ext-auth-core</module>
|
<module>flash-ext-auth-core</module>
|
||||||
<module>flash-ext-oidc</module>
|
<module>flash-ext-auth-oidc</module>
|
||||||
<module>flash-ext-routeviewer</module>
|
<module>flash-ext-routeviewer</module>
|
||||||
<module>flash-ext-view-core</module>
|
<module>flash-ext-view-core</module>
|
||||||
<module>flash-ext-view-jte</module>
|
<module>flash-ext-view-jte</module>
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>dev.relism</groupId>
|
<groupId>dev.relism</groupId>
|
||||||
<artifactId>flash-ext-oidc</artifactId>
|
<artifactId>flash-ext-auth-oidc</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
Reference in New Issue
Block a user