Skip to content

Overview ​

OAG is built in an extendable way. This means that for many aspects you can develop you own implementation of certain functionality and "hook" them into OAG. This section introduces you to how you can create and hook in your code for the different aspects.

How an extension is selected ​

An extension is a Spring bean that declares the configuration value selecting it. Every selectable extension implements org.owasp.oag.extension.TypedExtension:

java
String type();

At startup OAG collects all beans of an extension point into a type registry (org.owasp.oag.extension.TypeRegistry). The registry maps each type() to its implementation and resolves the type value from the configuration file through it. The bean name plays no role. Two implementations declaring the same type, or a configured type that no implementation declares, stop OAG at startup with an error that lists the known types.

Where creating the extension needs configuration settings or a fresh instance per request, the registered bean is a factory that declares the type and creates the extension. Stateless extensions register themselves.

mermaid
flowchart LR
    cfg["configuration<br/>type: foo"] --> reg[TypeRegistry]
    reg -->|"type() == foo"| impl["extension or factory<br/>@Component"]
    impl -.->|factory only| ext[created extension]
Extension pointRegistered bean (declares type())RegistryConfiguration value
LoginDriverLoginDriverFactoryLoginDriverRegistryloginProviders.<name>.type
UserMapperUserMappingFactoryUserMappingRegistrysecurityProfiles.<name>.userMapping.type
JwtSignerJwtSignerFactoryJwtSignerRegistryuserMapping.settings.signatureImplementation
CsrfProtectionValidationthe validation itselfCsrfValidationRegistrysecurityProfiles.<name>.csrfProtection
TraceContextTraceContextFactoryTraceContextRegistrytraceProfile.type
JwkStorethe store itselfJwkStoreRegistrykeyManagementProfile.jwkStoreProfile.type
KeyGeneratorthe generator itselfKeyGeneratorRegistrykeyManagementProfile.keyGeneratorProfile.type
KeyRotationthe rotation itselfKeyRotationRegistrykeyManagementProfile.keyRotationProfile.type

A minimal custom extension therefore looks like this:

java
@Component
public class MyCsrfValidation implements CsrfProtectionValidation {

    @Override
    public String type() {
        return "mySpecial";             // csrfProtection: mySpecial
    }
    // ...
}

SessionHook and SessionBlacklist are not selected by type; this is noted per interface below.

How configured types are validated ​

Configuration validation runs in two stages when the application context is ready (org.owasp.oag.config.MainConfigValidator):

  1. Structure. Each configuration class (org.owasp.oag.config.configuration) implements ErrorValidation and reports missing or invalid values of its own fields. It knows nothing about registered extensions.
  2. Extension types. Only if the structure is valid, every ConfigValidator bean checks its part of the configuration against its registry and reports unknown types with the configuration entry and the known types, for example LoginProvider 'github': Unknown type 'githb'. Known types: [github, oidc].
mermaid
flowchart LR
    start[context ready] --> mcv[MainConfigValidator]
    mcv -->|1| ev["MainConfig.getErrors()<br/>structure"]
    mcv -->|"2, if no errors"| cv["ConfigValidator beans<br/>one per extension point"]
    cv --> reg[registries]
ValidatorChecks
LoginProviderValidatorloginProviders.<name>.type, and that a driver can be created from the settings
UserMappingValidatorsecurityProfiles.<name>.userMapping.type
CsrfProtectionValidatorsecurityProfiles.<name>.csrfProtection
TraceProfileValidatortraceProfile.type
KeyManagementValidatorthe type of jwkStoreProfile, keyGeneratorProfile and keyRotationProfile

A new implementation of an existing extension point only declares its type(); it is validated automatically. A new extension point adds a ConfigValidator next to its registry, so that configuration classes never depend on extension packages. Extension points whose beans resolve their type while they are created (user mapping, JWT signer, key management, trace) can fail with the registry's error message before this validation runs.

Extensions run on the request's virtual thread and use the servlet API (jakarta.servlet.http). They are plain sequential Java — blocking calls are fine.

Reading the state of the current request ​

Some extension points receive everything they need as a parameter — a UserMapper gets the session through GatewayRouteContext, a SessionHook gets the shared filterContext map. Where that is not the case, the state of the current request is available in scoped values that OAG binds around the whole request:

java
Optional<Session> session = Session.REQUEST_SESSION.get();     // org.owasp.oag.session.Session
TraceHeaders traceHeaders = TraceContext.REQUEST_TRACE_HEADERS.get(); // org.owasp.oag.logging.TraceContext

REQUEST_SESSION is bound for every request, whether authenticated or not; it is empty when the request carries no valid session.

A Session holds the values of the decrypted session cookie: user model, login provider, session id, expiry and the CSRF token bound to the session (getCsrfToken()).

These values are only readable while a request is being handled. Calling get() from code that runs outside a request, such as a scheduled task, throws NoSuchElementException; use isBound() first if your class can run in both situations.

There is no need to pass the request around for logging: the trace id of the request is bound for its whole duration, so a plain log.info(...) from anywhere in your extension is written with the correct trace id.

Extension points ​

org.owasp.oag.services.csrf.CsrfProtectionValidation ​

Decides whether a request is blocked as a suspected CSRF attack.

java
boolean needsRequestBody();
boolean shouldBlockRequest(HttpServletRequest request, String requestBody);

Called by the CSRF route filters for every proxied request whose method is not listed in the security profile's csrfSafeMethods. needsRequestBody() selects which of the two filters applies: when it returns false the request is validated before the body is read and requestBody is null; when it returns true the body is buffered first and passed in.

The session is not passed in; read it from Session.REQUEST_SESSION as described above and compare the submitted token with session.getCsrfToken(). The built-in implementations only block a request that has a session — an anonymous request cannot carry a session to abuse.

Selected through the security profile's csrfProtection value, which must match type(). Built-in types: none, sameSiteStrictCookie, doubleSubmitCookie, doubleSubmitCookieWithBody.

org.owasp.oag.services.tokenMapping.UserMapper ​

Adds the authenticated user's identity to the request that is forwarded downstream.

java
ServerRequest mapUserToRequest(ServerRequest request, GatewayRouteContext context);

ServerRequest is org.springframework.web.servlet.function.ServerRequest and GatewayRouteContext is org.owasp.oag.filters.GatewayRouteContext, which carries the route name, the route and security-profile configuration, the request and upstream URI, and the optional org.owasp.oag.session.Session.

Called by the user-mapping route filter once per proxied request, after access control and CSRF validation. ServerRequest is immutable, so an implementation returns a new request built with ServerRequest.from(request); returning the argument unchanged adds no user information.

Created by a org.owasp.oag.services.tokenMapping.UserMappingFactory (UserMapper load(Map<String, Object> settings)) once per security profile at startup. The factory is selected through the userMapping type value, which must match the factory's type(). Built-in types: jwtToken, requestHeader, no.

org.owasp.oag.hooks.session.SessionHook ​

Participates in the session lifecycle, typically to set or clear cookies.

java
void createSession(Map<String, Object> filterContext, HttpServletResponse response);
void renewSession(Map<String, Object> filterContext, HttpServletResponse response);
void destroySession(Map<String, Object> filterContext, HttpServletRequest request, HttpServletResponse response);
int order();

Called by org.owasp.oag.hooks.session.SessionHookChain when a user logs in (createSession), when a rolling session is extended because it has less than renewWhenLessThan seconds left (renewSession), and when a user logs out (destroySession). The filterContext map is shared between all hooks of one invocation and is how hooks pass values to each other; order() fixes the sequence, lowest first.

Every SessionHook bean in the application context takes part — there is no type setting to select one.

org.owasp.oag.services.blacklist.SessionBlacklist ​

Keeps track of session identifiers that must no longer be accepted. Extends java.io.Closeable.

java
void invalidateSession(String identifier, int ttl);
boolean isInvalidated(String identifier);
void cleanup();

invalidateSession is called on logout and must keep the identifier blacklisted for at least ttl seconds. isInvalidated is called by the session-extraction servlet filter for every request that presents a session cookie. cleanup drops expired entries; OAG schedules it on a background thread every two session durations (sessionBehaviour.sessionDuration), so an implementation must be safe to call concurrently with invalidateSession and isInvalidated, and must not lose an identifier that is invalidated while a cleanup pass is running. All three are blocking calls.

org.owasp.oag.services.login.drivers.LoginDriver ​

Implements authentication against one identity provider.

java
LoginDriverResult startLogin(URI callbackUri);
UserModel processCallback(HttpServletRequest request, String state, URI callbackUri) throws AuthenticationException;
URL processFederatedLogout(UserModel userModel);
List<String> getSettingsErrors(LoginProviderSettings settings);
default boolean settingsAreValid(LoginProviderSettings settings);

startLogin is called when a client hits /auth/{provider}/login and returns the redirect to the provider plus the generated state. processCallback is called when the provider redirects back to /auth/{provider}/callback; it validates state, exchanges the code for tokens and returns the org.owasp.oag.session.UserModel the session is built from. processFederatedLogout is called on logout and may return a URL to redirect to so the session also ends at the provider, or null to use redirectLogout. getSettingsErrors is called at startup to validate the configuration.

A driver is created by a org.owasp.oag.services.login.drivers.LoginDriverFactory (LoginDriver load(LoginProviderSettings settings)), selected through the login provider's type value, which must match the factory's type(). Built-in types: oidc, github.

org.owasp.oag.logging.TraceContext ​

Produces and propagates the trace id (correlation id) of a request.

java
void generateNewTraceId();
void applyExistingTrace(String primaryTraceInfo, String secondaryTraceInfo);
boolean sendTraceDownstream();
boolean forwardIncomingTrace();
boolean acceptAdditionalTraceInfo();
boolean sendTraceResponse();
String getMainRequestHeader();
String getSecondaryRequestHeader();
String getResponseHeader();
String getTraceString();
String getSecondaryTraceInfoString();
String getTraceResponseString();

A new instance is created per request by a org.owasp.oag.logging.TraceContextFactory (TraceContext create()), so a trace context is a plain object, not a bean. The tracing servlet filter then calls either applyExistingTrace (when a trace header arrives and forwardIncomingTrace is set) or generateNewTraceId, and reads the header names and values from the remaining methods to propagate the trace downstream and, optionally, back upstream. The getters throw org.owasp.oag.exception.TraceException when no trace has been set up.

The factory is selected through the traceProfile type value, which must match its type(). Built-in types: w3cTraceContext, simpleTraceContext, noTraceContext.

org.owasp.oag.services.crypto.jwt.JwtSignerFactory ​

Creates the signer that signs downstream user-mapping JWTs.

java
JwtSigner create(String hostUri, Map<String, Object> settings);

Called by the JWT user mapping when it initialises, once per security profile. settings is the raw user-mapping settings map from the configuration file. The returned org.owasp.oag.services.crypto.jwt.JwtSigner is an abstract class responsible for signing and for its own key handling.

Selected through the signatureImplementation setting of a JWT user mapping, which must match type(). Built-in types: rsa, hmac.

org.owasp.oag.services.keymgm.JwkStore ​

Holds OAG's JWT signing keys and serves the public ones.

java
JWKSet getSigningPublicKey(String kid);
JWKSet getSigningPublicKeys();
void addRawKey(String kid, Key key, long expiry);
void addJwk(JWK key, long expiry);
void removeKey(String kid);

JWKSet and JWK are the Nimbus types com.nimbusds.jose.jwk.* and Key is java.security.Key. The add* methods are called by key rotation whenever a new signing key is generated, removeKey when a key has expired, and the two getters back the /.well-known/jwks and /.well-known/jwks/{kid} endpoints. expiry is a Unix timestamp.

Selected through keyManagementProfile.jwkStoreProfile.type, which must match type(). Built-in type: localRsaJwkStore.

org.owasp.oag.services.keymgm.KeyGenerator ​

Creates the keys used to sign JWTs.

java
GeneratedKey generateJWTSigningKey();

Called by key rotation at startup and on every rotation interval. GeneratedKey is the nested class org.owasp.oag.services.keymgm.KeyGenerator.GeneratedKey; it wraps either a java.security.KeyPair (asymmetric signing, e.g. RS256) or a single java.security.Key (symmetric signing, e.g. HS256), and exposes getKeyUsedToVerify(), getTypeString() and equalType(GeneratedKey).

Selected through keyManagementProfile.keyGeneratorProfile.type, which must match type(). Built-in type: rsaKeyGenerator.

org.owasp.oag.services.keymgm.KeyRotation ​

Rotates the JWT signing keys.

java
void start();

Called once at startup, after all beans are created and before the gateway serves requests, and only on the implementation selected through keyManagementProfile.keyRotationProfile.type. It must generate the first signing key, assign it in org.owasp.oag.services.keymgm.CurrentSigningKeyHolder, publish it in the JwkStore, and schedule further rotations itself. Built-in type: defaultKeyRotation.

Reading the request body in an extension ​

A custom extension must not call HttpServletRequest.getParameter(...), getParameterMap() or anything else that triggers parameter parsing on a request that has a body.

For a POST with form content the parameters are already available and reading them is harmless. For any other method, parameter parsing consumes the request body, and OAG has no filter that rebuilds it — the downstream system would receive an empty body, with nothing failing visibly to indicate why.

Read the query string directly instead. org.owasp.oag.services.csrf.CsrfDoubleSubmitCookieValidation does exactly this to find its CSRF token, and is the pattern to copy:

java
var queryParams = UriComponentsBuilder.fromUriString("?" + request.getQueryString())
        .build()
        .getQueryParams();
var token = queryParams.getFirst("CSRFToken");

If an extension genuinely needs the body, use org.springframework.cloud.gateway.server.mvc.common.MvcUtils.getOrCacheBody(ServerRequest), which caches the body so it can still be forwarded, and pair it with BeforeFilterFunctions.adaptCachedBody() on the route so the proxy re-reads the cached copy. org.owasp.oag.filters.proxy.CsrfValidationFilterWithBody shows the complete arrangement.

Open-source Apache 2 Licensed | Powered by a lot of love ❤️ (and code)