Skip to content

Commit 7f2d50e

Browse files
committed
Support ValidRedirectUrls
1 parent aa154ee commit 7f2d50e

9 files changed

Lines changed: 157 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
## [1.0.5] - 2026-03-15
13+
14+
### Added
15+
16+
- Support ValidRedirectUrls
17+
1218
## [1.0.4] - 2026-03-15
1319

1420
### Added

src/main/java/de/denniskniep/safed/common/config/FederationAppConfig.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
public abstract class FederationAppConfig extends AppConfig {
1010
private String clientId;
1111
private URL redirectUrl;
12+
private List<String> validRedirectUrls;
1213
private URL signInUrl;
1314
private List<ClaimConfig> claims = new ArrayList<>();
1415
private URL issuerId;
@@ -58,7 +59,6 @@ public void setClientId(String clientId) {
5859
this.clientId = clientId;
5960
}
6061

61-
// TODO: extract URI from request -> only validate provided with list of RedirectURIs
6262
public URL getRedirectUrl() {
6363
return redirectUrl;
6464
}
@@ -67,6 +67,14 @@ public void setRedirectUrl(URL redirectUrl) {
6767
this.redirectUrl = redirectUrl;
6868
}
6969

70+
public List<String> getValidRedirectUrls() {
71+
return validRedirectUrls;
72+
}
73+
74+
public void setValidRedirectUrls(List<String> validRedirectUrls) {
75+
this.validRedirectUrls = validRedirectUrls;
76+
}
77+
7078
public URL getSignInUrl() {
7179
return signInUrl;
7280
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package de.denniskniep.safed.common.utils;
2+
3+
import java.net.MalformedURLException;
4+
import java.net.URI;
5+
import java.net.URL;
6+
import java.util.List;
7+
8+
public class RedirectUtils {
9+
10+
public static URL getValidRedirectUrl(String redirectUri, List<String> validRedirectUrls){
11+
return getValidRedirectUrl(URI.create(redirectUri), validRedirectUrls);
12+
}
13+
14+
public static URL getValidRedirectUrl(URI redirectUri, List<String> validRedirectUrls){
15+
try {
16+
URL redirectUrl = redirectUri.toURL();
17+
String result = RedirectUtils.matchesRedirects(validRedirectUrls, redirectUri.toString(), true);
18+
if(result != null){
19+
return redirectUrl;
20+
}
21+
throw new RuntimeException("Redirect URI from request "+ redirectUri + " does not match one of the valid redirect URLs: " + String.join(", ", validRedirectUrls) );
22+
} catch (MalformedURLException e) {
23+
throw new RuntimeException("Redirect URI from request "+ redirectUri + " is malformed!", e);
24+
}
25+
}
26+
27+
public static String matchesRedirects(List<String> validRedirects, String redirect, boolean allowWildcards) {
28+
for (String validRedirect : validRedirects) {
29+
if ("*".equals(validRedirect)) {
30+
// the valid redirect * is a full wildcard for http(s) even if the redirect URI does not allow wildcards
31+
return validRedirect;
32+
} else if (validRedirect.endsWith("*") && !validRedirect.contains("?") && allowWildcards) {
33+
// strip off the query or fragment components - we don't check them when wildcards are effective
34+
int idx = redirect.indexOf('?');
35+
if (idx == -1) {
36+
idx = redirect.indexOf('#');
37+
}
38+
String r = idx == -1 ? redirect : redirect.substring(0, idx);
39+
// strip off *
40+
int length = validRedirect.length() - 1;
41+
validRedirect = validRedirect.substring(0, length);
42+
if (r.startsWith(validRedirect)) return validRedirect;
43+
// strip off trailing '/'
44+
if (length - 1 > 0 && validRedirect.charAt(length - 1) == '/') length--;
45+
validRedirect = validRedirect.substring(0, length);
46+
if (validRedirect.equals(r)) return validRedirect;
47+
} else if (validRedirect.equals(redirect)){
48+
return validRedirect;
49+
}
50+
}
51+
return null;
52+
}
53+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package de.denniskniep.safed.common.verifications;
2+
3+
import de.denniskniep.safed.common.scans.AuthResult;
4+
import de.denniskniep.safed.common.scans.ScanResultStatus;
5+
import org.springframework.stereotype.Service;
6+
7+
import java.util.ArrayList;
8+
import java.util.List;
9+
10+
@Service
11+
public class TitleInfo implements ScanResultVerificationStrategy {
12+
13+
@Override
14+
public List<String> extractInfos(AuthResult scanAuthResult) {
15+
if(scanAuthResult.getResponsePage().base64Screenshot() == null){
16+
return new ArrayList<>();
17+
}
18+
19+
return List.of(
20+
"[INFO] Title: " + scanAuthResult.getResponsePage().title()
21+
);
22+
}
23+
24+
@Override
25+
public VerificationResult evaluateScanResult(AuthResult firstPositiveAuthResult, AuthResult secondPositiveAuthResult, AuthResult scanAuthResult) {
26+
return new VerificationResult(ScanResultStatus.OK, List.of());
27+
}
28+
}

src/main/java/de/denniskniep/safed/oidc/auth/server/OidcFlow.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import de.denniskniep.safed.common.auth.browser.HttpRequest;
44
import de.denniskniep.safed.common.config.ClaimConfig;
55
import de.denniskniep.safed.common.utils.KeyProvider;
6+
import de.denniskniep.safed.common.utils.RedirectUtils;
67
import de.denniskniep.safed.oidc.auth.browser.OidcAuthenticationRequest;
78
import de.denniskniep.safed.oidc.auth.server.endpoints.TokenRequest;
89
import de.denniskniep.safed.oidc.auth.server.endpoints.TokenResponse;
@@ -18,6 +19,7 @@
1819
import java.net.URI;
1920
import java.net.URISyntaxException;
2021

22+
import java.net.URL;
2123
import java.time.Duration;
2224
import java.time.Instant;
2325
import java.time.temporal.ChronoUnit;
@@ -57,18 +59,25 @@ public HttpRequest buildWebRequest() {
5759
responseMode = "query";
5860
}
5961

62+
URL redirectUrl;
63+
if(clientConfig.getRedirectUrl() != null) {
64+
redirectUrl = clientConfig.getRedirectUrl();
65+
} else{
66+
redirectUrl = RedirectUtils.getValidRedirectUrl(requestData.getRedirectUri(), clientConfig.getValidRedirectUrls());
67+
}
68+
6069
if(StringUtils.equalsIgnoreCase(responseMode, "query")) {
61-
URI uri = buildQuery(clientConfig.getRedirectUrl().toString(), params);
70+
URI uri = buildQuery(redirectUrl.toString(), params);
6271
return new HttpRequest("GET", uri.toString());
6372
}
6473

6574
if(StringUtils.equalsIgnoreCase(responseMode, "form_post")) {
66-
URI uri = buildQuery(clientConfig.getRedirectUrl().toString());
75+
URI uri = buildQuery(redirectUrl.toString());
6776
return new HttpRequest("POST", uri.toString(), params);
6877
}
6978

7079
if(StringUtils.equalsIgnoreCase(responseMode, "fragment")) {
71-
URI uri = buildQuery(clientConfig.getRedirectUrl().toString(), new HashedMap<>(), params);
80+
URI uri = buildQuery(redirectUrl.toString(), new HashedMap<>(), params);
7281
return new HttpRequest("GET", uri.toString());
7382
}
7483

src/main/java/de/denniskniep/safed/saml/auth/browser/SamlInitializationResult.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public SamlRequestData asSamlRequestData() {
3535
SamlRequestData samlRequestData = new SamlRequestData();
3636
samlRequestData.setId(samlRequest.getID());
3737
samlRequestData.setRelayState(RelayState);
38+
samlRequestData.setRedirectUri(samlRequest.getAssertionConsumerServiceURL());
3839
return samlRequestData;
3940
}
4041
}

src/main/java/de/denniskniep/safed/saml/auth/browser/SamlRequestData.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package de.denniskniep.safed.saml.auth.browser;
22

3+
import java.net.URI;
4+
35
public class SamlRequestData {
46
private String id;
57
private String relayState;
8+
private URI redirectUri;
69

710
public String getId() {
811
return id;
@@ -19,4 +22,12 @@ public String getRelayState() {
1922
public void setRelayState(String relayState) {
2023
this.relayState = relayState;
2124
}
25+
26+
public URI getRedirectUri() {
27+
return redirectUri;
28+
}
29+
30+
public void setRedirectUri(URI redirectUri) {
31+
this.redirectUri = redirectUri;
32+
}
2233
}

src/main/java/de/denniskniep/safed/saml/auth/server/SamlResponseBuilder.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package de.denniskniep.safed.saml.auth.server;
22

33
import de.denniskniep.safed.common.utils.KeyProvider;
4+
import de.denniskniep.safed.common.utils.RedirectUtils;
45
import de.denniskniep.safed.saml.config.SamlAppConfig;
56
import de.denniskniep.safed.saml.config.SamlAuthData;
67
import de.denniskniep.safed.saml.auth.browser.SamlRequestData;
@@ -15,6 +16,7 @@
1516
import org.w3c.dom.Document;
1617

1718
import java.net.URI;
19+
import java.net.URL;
1820
import java.security.PrivateKey;
1921
import java.security.PublicKey;
2022

@@ -43,10 +45,17 @@ public SamlResponseResult create(SamlAppConfig clientConfig, SamlRequestData req
4345
// SamlProtocol.authenticated
4446
// SamlService.handleSamlRequest
4547

48+
URL redirectUrl;
49+
if(clientConfig.getRedirectUrl() != null) {
50+
redirectUrl = clientConfig.getRedirectUrl();
51+
} else{
52+
redirectUrl = RedirectUtils.getValidRedirectUrl(request.getRedirectUri(), clientConfig.getValidRedirectUrls());
53+
}
54+
4655
// Manipulate standard properties
4756
SAML2LoginResponseBuilder builder = new SAML2LoginResponseBuilder()
4857
.requestID(request.getId()) // InResponseTo
49-
.destination(clientConfig.getRedirectUrl().toExternalForm())
58+
.destination(redirectUrl.toString())
5059
.issuer(clientConfig.getIssuerId().toExternalForm())
5160
.assertionExpiration(clientConfig.getAssertionLifespanInMinutes())
5261
.subjectExpiration(clientConfig.getAssertionLifespanInMinutes())
@@ -133,7 +142,7 @@ public SamlResponseResult create(SamlAppConfig clientConfig, SamlRequestData req
133142
var postBindingBuilder = bindingBuilder.postBinding(samlDocument);
134143

135144
//todo: Apply here XML Changes after signature!
136-
return postBindingBuilder.createWebRequest(clientConfig.getRedirectUrl());
145+
return postBindingBuilder.createWebRequest(redirectUrl);
137146
} catch (Exception e) {
138147
throw new RuntimeException("Can not build SAMLResponse", e);
139148
}

src/main/resources/application-dev.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ oidc:
2828
client-id: example-oidc-002-implicitflow
2929
sign-in-url: "http://localhost:8084/"
3030
redirect-url: "http://localhost:8084/oauth/implicit"
31+
example-oidc-002-implicitflow-valid-redirect-uris:
32+
issuer-id: http://keycloak:8080/realms/demo
33+
issuer-endpoint-url: http://keycloak:8080/realms/demo/protocol/openid-connect/auth
34+
signing-private-key-pem-file-path: "./dev/keycloak/signing_key.pem"
35+
signing-x509-cert-pem-file-path: "./dev/keycloak/signing_cert.pem"
36+
client-id: example-oidc-002-implicitflow
37+
sign-in-url: "http://localhost:8084/"
38+
valid-redirect-urls:
39+
- "http://localhost:8084/*"
3140
grafana-oidc:
3241
issuer-id: http://keycloak:8080/realms/demo
3342
issuer-endpoint-url: http://keycloak:8080/realms/demo/protocol/openid-connect/auth
@@ -55,6 +64,23 @@ saml:
5564
sign-in-url: "http://localhost:8081/"
5665
redirect-url: "http://localhost:8081/login/saml2/sso/example-saml-001"
5766

67+
example-saml-001-valid-redirect-uris:
68+
claims:
69+
- name: "name"
70+
values:
71+
- "Pen Tester"
72+
- name: "given_name"
73+
values:
74+
- "Tester"
75+
issuer-id: http://keycloak:8080/realms/demo
76+
issuer-endpoint-url: http://keycloak:8080/realms/demo/protocol/saml
77+
signing-private-key-pem-file-path: "./dev/keycloak/signing_key.pem"
78+
signing-x509-cert-pem-file-path: "./dev/keycloak/signing_cert.pem"
79+
client-id: example-saml-001
80+
sign-in-url: "http://localhost:8081/"
81+
valid-redirect-urls:
82+
- "http://localhost:8081/login/saml2/sso/*"
83+
5884
example-saml-001-no-auto-redirect:
5985
claims:
6086
- name: "name"

0 commit comments

Comments
 (0)