Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ coverage/

# Personal files
.idea
.vscode

# System files
.DS_Store

# ---------------------------------------------------------------------
# Add specific rules here…

# Environment files — never commit secrets. Track a .env.example template instead.
.env
89 changes: 89 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,94 @@
# MIGRATION

## V 2.x (Spring Boot 3) to V 2.2 (Spring Boot 4)

`kinde-springboot-starter` and `kinde-springboot-core` were upgraded to
[Spring Boot 4.0.x](https://spring.io/blog/2025/11/20/spring-boot-4-0-0-available-now)
and [Spring Security 7.x](https://docs.spring.io/spring-security/reference/7.0.0/migration/index.html).
If you depend on either, here is what changes for you.

### Baseline requirements

- **Java 17 or later.** Spring Boot 4 raised the minimum JVM. The SDK
itself is built on Java 25 but produces Java 17-compatible bytecode.
- **Spring Boot 4.x** in your application. Spring Boot 3.x consumers
should stay on the previous Kinde SDK release line.

### Behaviour change: JWT audience validation is now opt-in

Previously the resource-server JWT validator was hardcoded to require an
audience claim of `"api://default"` (a leftover sample value). Real Kinde
access tokens carry an empty `aud` array unless you explicitly configure
an API resource on the Kinde dashboard, so the previous default rejected
every default Kinde token with a 401:

```
"This aud claim is not equal to the configured audience"
```

The validator now skips audience checking unless you explicitly configure
an expected audience:

```yaml
kinde:
oauth2:
audience: https://your-api.example.com # optional; only set if you
# configured an API resource
# on the Kinde dashboard
```

If you previously relied on the implicit default and your tokens actually
contained `api://default` as their audience (highly unlikely for Kinde
deployments), set `kinde.oauth2.audience: api://default` to preserve the
old behaviour.

### Configuration property rename: `okta.oauth2.post-logout-redirect-uri` → `kinde.oauth2.post-logout-redirect-uri`

RP-Initiated logout (redirect to the IdP's logout endpoint after a local
sign-out) is wired in by the SDK when this property is set. The previous
property name was an Okta-fork leftover that nobody in a Kinde deployment
could plausibly have set. To enable RP-Initiated logout now:

```yaml
kinde:
oauth2:
post-logout-redirect-uri: http://localhost:8080 # where to land
# after Kinde
# clears its
# session
```

The same rename applies to the reactive (WebFlux) auto-configuration.

### New transitive dependencies for `kinde-springboot-starter`

Spring Boot 4 split OAuth2 auto-configuration out of
`spring-boot-autoconfigure` into dedicated starters. The
`kinde-springboot-starter` pom now pulls them in for you:

- `spring-boot-starter-oauth2-client`
- `spring-boot-starter-oauth2-resource-server`

You do **not** need to add them to your own pom unless you previously
declared them explicitly and want to keep that explicit (which is fine).

### Spring Security 7 source-level changes (only affects custom code)

If you previously customized security by writing your own
`SecurityFilterChain` against Spring Security 6 APIs, some method
signatures have changed in Spring Security 7. Notable items:

- `oauth2Login(Customizer)` and `oauth2ResourceServer(Customizer)`
signatures changed slightly; the lambda forms still work but the
no-arg deprecated forms are gone.
- `WebSecurityConfigurerAdapter` (long deprecated) is fully removed.
- Several token-endpoint client classes moved packages. The SDK does
this internally; you are only affected if you wired your own
`OAuth2AccessTokenResponseClient` bean.

The [official Spring Security 7 migration guide](https://docs.spring.io/spring-security/reference/7.0.0/migration/index.html)
is the source of truth.

## V 1.0.0 to V 2.0.0

The original implementation was based on Spring Boot controllers. It provided a controller that could be instantiated inline and then invoked to perform the PKCE authentication. Version 2.0.0 implements a core OAuth and OpenID library. This library provides a rich set of functions, from token management to OpenID authentication.
Expand Down
1 change: 0 additions & 1 deletion kinde-core/.env

This file was deleted.

41 changes: 41 additions & 0 deletions kinde-core/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Template for kinde-core .env. Copy to `.env` and customise as needed.
# `.env` is gitignored; this template is committed so contributors have a starting point.
#
# kinde-core is the SDK library. Most callers configure it programmatically via
# KindeClientBuilder, but the SDK will also pick up these variables from .env or
# the process environment at runtime. The full list of supported keys is defined
# in com.kinde.config.KindeParameters.

# === Tenant + credentials ===
KINDE_DOMAIN=https://<your-subdomain>.kinde.com
KINDE_CLIENT_ID=<your-kinde-client-id>
KINDE_CLIENT_SECRET=<your-kinde-client-secret>

# === OAuth flow ===
# AuthorizationType values: CODE | PKCE | CLIENT_CREDENTIALS | TOKEN
KINDE_GRANT_TYPE=CODE
KINDE_SCOPES=openid
KINDE_REDIRECT_URI=http://localhost:8080/callback
KINDE_LOGOUT_REDIRECT_URI=http://localhost:8080

# === Optional ===
# Audience claim (comma-separated). Required when calling the Kinde Management API.
# KINDE_AUDIENCE=https://<your-subdomain>.kinde.com/api

# UI language hint passed to Kinde (e.g. en, fr, de)
# KINDE_LANG=en

# Scope auth to a specific organisation
# KINDE_ORG_CODE=

# Whether Kinde redirects to a success page after registration
# KINDE_HAS_SUCCESS_PAGE=false

# Auth protocol override (typically derived by the SDK)
# KINDE_PROTOCOL=

# === Endpoint overrides (rarely needed — defaults derive from KINDE_DOMAIN) ===
# KINDE_OPENID_ENDPOINT=https://<your-subdomain>.kinde.com/.well-known/openid-configuration
# KINDE_AUTHORIZATION_ENDPOINT=https://<your-subdomain>.kinde.com/oauth2/auth
# KINDE_TOKEN_ENDPOINT=https://<your-subdomain>.kinde.com/oauth2/token
# KINDE_LOGOUT_ENDPOINT=https://<your-subdomain>.kinde.com/logout
44 changes: 8 additions & 36 deletions kinde-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,33 +15,6 @@
<url>http://maven.apache.org</url>

<dependencies>
<!-- https://mvnrepository.com/artifact/com.nimbusds/oauth2-oidc-sdk -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>oauth2-oidc-sdk</artifactId>
</dependency>

<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<!-- JUnit 5 API and Engine -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.nimbusds/oauth2-oidc-sdk -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>oauth2-oidc-sdk</artifactId>
Expand Down Expand Up @@ -71,15 +44,13 @@
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>

<!-- Optional: For parameterized tests -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>

<!-- Mockito (if you are using it for mocking) -->
<!-- Mockito -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
Expand All @@ -91,36 +62,37 @@
<scope>test</scope>
</dependency>


<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<!-- {version} can be 6.0.0, 7.0.0, etc. -->
<exclusions>
<exclusion>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>


<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<!-- Test-only SLF4J provider so the no-provider warning is silenced.
Version + scope inherited from parent pom dependencyManagement. -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.github.cdimascio</groupId>
<artifactId>dotenv-java</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,18 @@ public KindeTokens retrieveTokens() {
throw new Exception("Access token validation failed: " + e.getMessage(), e);
}
} else if (this.kindeToken instanceof com.kinde.token.RefreshToken) {
// If we have a refresh token, perform the token exchange
// But first check if we have the necessary configuration
if (this.kindeConfig.tokenEndpoint() == null || this.kindeConfig.tokenEndpoint().isEmpty()) {
// Resolve the token endpoint: prefer an explicit KINDE_TOKEN_ENDPOINT override
// and fall back to the OIDC-discovered endpoint exposed via OidcMetaData. This
// matches the behavior of sibling code (KindeClientSessionImpl,
// KindeClientCodeSessionImpl) which already rely on discovery via
// `oidcMetaData.getOpMetadata().getTokenEndpointURI()`.
URI tokenEndpoint = resolveTokenEndpoint();
if (tokenEndpoint == null) {
throw new Exception("Token endpoint not configured - cannot exchange refresh token");
}

com.kinde.token.RefreshToken refreshToken = (com.kinde.token.RefreshToken) this.kindeToken;

// Create the token request using the correct approach
RefreshToken nimbusRefreshToken = new RefreshToken(refreshToken.token());
AuthorizationGrant refreshTokenGrant = new RefreshTokenGrant(nimbusRefreshToken);
Expand All @@ -96,8 +100,6 @@ public KindeTokens retrieveTokens() {
Secret clientSecret = new Secret(this.kindeConfig.clientSecret());
ClientAuthentication clientAuth = new ClientSecretBasic(clientID, clientSecret);

// Convert the token endpoint string to URI and create the request
URI tokenEndpoint = URI.create(this.kindeConfig.tokenEndpoint());
TokenRequest request = new TokenRequest(tokenEndpoint, clientAuth, refreshTokenGrant);

HTTPRequest httpRequest = request.toHTTPRequest();
Expand Down Expand Up @@ -151,4 +153,22 @@ public UserInfo retrieveUserInfo() {

return new UserInfo(userInfoResponse.toSuccessResponse().getUserInfo());
}

/**
* Returns the token endpoint URI to use for refresh-token exchanges. Honours an explicit
* override on {@link KindeConfig#tokenEndpoint()} (sourced from
* {@code KINDE_TOKEN_ENDPOINT}) and otherwise falls back to the OIDC-discovered endpoint
* on {@link OidcMetaData}. Returns {@code null} when neither is available, which is treated
* as a configuration error by the caller.
*/
private URI resolveTokenEndpoint() {
String configured = this.kindeConfig.tokenEndpoint();
if (configured != null && !configured.isEmpty()) {
return URI.create(configured);
}
if (this.oidcMetaData != null && this.oidcMetaData.getOpMetadata() != null) {
return this.oidcMetaData.getOpMetadata().getTokenEndpointURI();
}
return null;
}
}
7 changes: 3 additions & 4 deletions kinde-core/src/main/java/com/kinde/token/BaseToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -554,11 +554,10 @@ public boolean hasAny(List<String> permissions, List<String> roles, List<String>
// ========== Helper Methods ==========

/**
* Gets roles from the token using the typed accessor.
* This method is now deprecated in favor of using token.getRoles() directly.
*
* Internal helper that returns the token's roles claim with null-safety,
* falling back to an empty list when the token or roles are absent.
*
* @return List of role strings, or empty list if no roles are found
* @deprecated Use token.getRoles() instead
*/
private List<String> getTokenRoles() {
List<String> roles = (token != null) ? getRoles() : null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,6 @@ public void setUp() {
}
""")));

///oauth2/token
System.out.println("Instanciate the wiremock service");
}


Expand Down Expand Up @@ -204,7 +202,6 @@ public void testRegisterUrlRequestTest() {
AuthorizationUrl authorizationUrl1 = kindeClientSession.register();
assertNotNull(authorizationUrl1);
assertNotNull(authorizationUrl1.getUrl());
System.out.println(authorizationUrl1.getUrl());
assertTrue(authorizationUrl1.getUrl().toString().contains("prompt=create"));
assertTrue(authorizationUrl1.getCodeVerifier() == null);

Expand All @@ -223,7 +220,6 @@ public void testRegisterUrlRequestTest() {
AuthorizationUrl authorizationUrl2 = kindeClientSession2.register();
assertNotNull(authorizationUrl2);
assertNotNull(authorizationUrl2.getUrl());
System.out.println(authorizationUrl2.getUrl());
assertTrue(authorizationUrl2.getUrl().toString().contains("prompt=create"));
assertTrue(authorizationUrl2.getUrl().toString().contains("org_code=TEST"));
assertTrue(authorizationUrl2.getUrl().toString().contains("has_success_page=true"));
Expand Down Expand Up @@ -258,7 +254,6 @@ public void testOrgCreateUrlRequestTest() {
AuthorizationUrl authorizationUrl1 = kindeClientSession.createOrg("TEST1");
assertNotNull(authorizationUrl1);
assertNotNull(authorizationUrl1.getUrl());
System.out.println(authorizationUrl1.getUrl());
assertTrue(authorizationUrl1.getUrl().toString().contains("prompt=create"));
assertTrue(authorizationUrl1.getUrl().toString().contains("org_name=TEST1"));
assertTrue(authorizationUrl1.getCodeVerifier() == null);
Expand All @@ -278,7 +273,6 @@ public void testOrgCreateUrlRequestTest() {
AuthorizationUrl authorizationUrl2 = kindeClientSession2.createOrg("TEST2");
assertNotNull(authorizationUrl2);
assertNotNull(authorizationUrl2.getUrl());
System.out.println(authorizationUrl2.getUrl());
assertTrue(authorizationUrl2.getUrl().toString().contains("prompt=create"));
assertTrue(authorizationUrl2.getUrl().toString().contains("org_code=TEST"));
assertTrue(authorizationUrl2.getUrl().toString().contains("org_name=TEST2"));
Expand Down
8 changes: 0 additions & 8 deletions kinde-core/src/test/java/com/kinde/token/IDTokenTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,6 @@ public void testIDTokenTest() throws Exception {
assertTrue( kindeToken2.token().equals(token2) );
assertTrue( kindeToken2.valid() );


String tokenString = JwtGenerator.generateIDToken();
System.out.println(tokenString);

KindeToken kindeToken4 = IDToken.init(tokenString,true);

System.out.println(kindeToken4.getPermissions());

assertTrue(kindeToken2.getStringFlag("test_str").equals("test_str"));
assertTrue(kindeToken2.getIntegerFlag("test_integer").equals(1));
assertTrue(kindeToken2.getBooleanFlag("test_boolean").equals(false));
Expand Down
4 changes: 4 additions & 0 deletions kinde-core/src/test/resources/simplelogger.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# SLF4J Simple provider configuration for tests.
# Default to WARN so the no-provider warning is gone but CI output stays clean.
# Raise to "info"/"debug" locally when troubleshooting a specific test.
org.slf4j.simpleLogger.defaultLogLevel=warn
4 changes: 0 additions & 4 deletions kinde-j2ee/.env

This file was deleted.

Loading
Loading