Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 17 additions & 0 deletions .env.release.example
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,23 @@ SKILLHUB_AUTH_DIRECT_ENABLED=false
SKILLHUB_WEB_AUTH_DIRECT_ENABLED=false
SKILLHUB_WEB_AUTH_DIRECT_PROVIDER=

# CAS 2.0 / 3.0 SSO. To enable:
# - SKILLHUB_AUTH_CAS_ENABLED=true
# - SKILLHUB_AUTH_CAS_SERVER_URL=https://cas.example.com (CAS server base URL, must be HTTPS)
# - SKILLHUB_AUTH_CAS_SERVICE_URL=https://skillhub.example.com/api/v1/auth/cas/callback
# (must equal ${SKILLHUB_PUBLIC_BASE_URL}/api/v1/auth/cas/callback)
# Once enabled, /api/v1/auth/methods exposes a CAS_REDIRECT entry to the web UI automatically.
SKILLHUB_AUTH_CAS_ENABLED=false
SKILLHUB_AUTH_CAS_SERVER_URL=
SKILLHUB_AUTH_CAS_SERVICE_URL=
SKILLHUB_AUTH_CAS_PROTOCOL_VERSION=3.0
# Override CAS attribute names if your server does not return uid/cn/mail.
SKILLHUB_AUTH_CAS_ATTR_USERNAME=uid
SKILLHUB_AUTH_CAS_ATTR_DISPLAY_NAME=cn
SKILLHUB_AUTH_CAS_ATTR_EMAIL=mail
# Development-only: allow http:// CAS server / service URLs. Never set this in production.
SKILLHUB_AUTH_CAS_ALLOW_INSECURE=false

# SMTP configuration for password reset verification emails.
SPRING_MAIL_HOST=
SPRING_MAIL_PORT=587
Expand Down
8 changes: 8 additions & 0 deletions compose.release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ services:
SKILLHUB_SECURITY_SCANNER_URL: http://skill-scanner:8000
SKILLHUB_SECURITY_SCANNER_MODE: upload
SKILLHUB_AUTH_DIRECT_ENABLED: ${SKILLHUB_AUTH_DIRECT_ENABLED:-false}
SKILLHUB_AUTH_CAS_ENABLED: ${SKILLHUB_AUTH_CAS_ENABLED:-false}
SKILLHUB_AUTH_CAS_SERVER_URL: ${SKILLHUB_AUTH_CAS_SERVER_URL:-}
SKILLHUB_AUTH_CAS_SERVICE_URL: ${SKILLHUB_AUTH_CAS_SERVICE_URL:-}
SKILLHUB_AUTH_CAS_PROTOCOL_VERSION: ${SKILLHUB_AUTH_CAS_PROTOCOL_VERSION:-3.0}
SKILLHUB_AUTH_CAS_ALLOW_INSECURE: ${SKILLHUB_AUTH_CAS_ALLOW_INSECURE:-false}
SKILLHUB_AUTH_CAS_ATTR_USERNAME: ${SKILLHUB_AUTH_CAS_ATTR_USERNAME:-uid}
SKILLHUB_AUTH_CAS_ATTR_DISPLAY_NAME: ${SKILLHUB_AUTH_CAS_ATTR_DISPLAY_NAME:-cn}
SKILLHUB_AUTH_CAS_ATTR_EMAIL: ${SKILLHUB_AUTH_CAS_ATTR_EMAIL:-mail}
BOOTSTRAP_ADMIN_ENABLED: ${BOOTSTRAP_ADMIN_ENABLED:-false}
BOOTSTRAP_ADMIN_USER_ID: ${BOOTSTRAP_ADMIN_USER_ID:-docker-admin}
BOOTSTRAP_ADMIN_USERNAME: ${BOOTSTRAP_ADMIN_USERNAME:-admin}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.iflytek.skillhub.service;

import com.iflytek.skillhub.auth.bootstrap.PassiveSessionAuthenticator;
import com.iflytek.skillhub.auth.cas.CasProperties;
import com.iflytek.skillhub.auth.direct.DirectAuthProvider;
import com.iflytek.skillhub.auth.oauth.OAuthLoginRedirectSupport;
import com.iflytek.skillhub.config.AuthSessionBootstrapProperties;
Expand All @@ -25,17 +26,20 @@ public class AuthMethodCatalog {
private final OAuth2ClientProperties oAuth2ClientProperties;
private final DirectAuthProperties directAuthProperties;
private final AuthSessionBootstrapProperties sessionBootstrapProperties;
private final CasProperties casProperties;
private final List<DirectAuthProvider> directAuthProviders;
private final List<PassiveSessionAuthenticator> passiveSessionAuthenticators;

public AuthMethodCatalog(OAuth2ClientProperties oAuth2ClientProperties,
DirectAuthProperties directAuthProperties,
AuthSessionBootstrapProperties sessionBootstrapProperties,
CasProperties casProperties,
List<DirectAuthProvider> directAuthProviders,
List<PassiveSessionAuthenticator> passiveSessionAuthenticators) {
this.oAuth2ClientProperties = oAuth2ClientProperties;
this.directAuthProperties = directAuthProperties;
this.sessionBootstrapProperties = sessionBootstrapProperties;
this.casProperties = casProperties;
this.directAuthProviders = directAuthProviders;
this.passiveSessionAuthenticators = passiveSessionAuthenticators;
}
Expand Down Expand Up @@ -102,6 +106,16 @@ public List<AuthMethodResponse> listMethods(String returnTo) {
)));
}

if (casProperties.isEnabled()) {
methods.add(new AuthMethodResponse(
"cas",
"CAS_REDIRECT",
"cas",
"CAS",
buildCasLoginUrl(sanitizedReturnTo)
));
}

return methods;
}

Expand All @@ -112,4 +126,12 @@ private String buildAuthorizationUrl(String registrationId, String returnTo) {
}
return baseUrl + "?returnTo=" + URLEncoder.encode(returnTo, StandardCharsets.UTF_8);
}

private String buildCasLoginUrl(String returnTo) {
String baseUrl = "/api/v1/auth/cas/login";
if (returnTo == null) {
return baseUrl;
}
return baseUrl + "?returnTo=" + URLEncoder.encode(returnTo, StandardCharsets.UTF_8);
}
}
13 changes: 13 additions & 0 deletions server/skillhub-app/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,19 @@ skillhub:
code-expiry: ${SKILLHUB_AUTH_PASSWORD_RESET_CODE_EXPIRY:PT10M}
email-from-address: ${SKILLHUB_AUTH_PASSWORD_RESET_FROM_ADDRESS:noreply@skillhub.local}
email-from-name: ${SKILLHUB_AUTH_PASSWORD_RESET_FROM_NAME:SkillHub}
cas:
enabled: ${SKILLHUB_AUTH_CAS_ENABLED:false}
# Base URL of the CAS server (e.g. https://cas.example.com). Must be HTTPS unless allow-insecure-server=true.
server-url: ${SKILLHUB_AUTH_CAS_SERVER_URL:}
# Must equal ${skillhub.public.base-url}/api/v1/auth/cas/callback — the CAS server validates
# the service ticket against this exact URL, so a mismatch yields INVALID_SERVICE.
service-url: ${SKILLHUB_AUTH_CAS_SERVICE_URL:}
protocol-version: ${SKILLHUB_AUTH_CAS_PROTOCOL_VERSION:3.0}
allow-insecure-server: ${SKILLHUB_AUTH_CAS_ALLOW_INSECURE:false}
attributes:
username: ${SKILLHUB_AUTH_CAS_ATTR_USERNAME:uid}
display-name: ${SKILLHUB_AUTH_CAS_ATTR_DISPLAY_NAME:cn}
email: ${SKILLHUB_AUTH_CAS_ATTR_EMAIL:mail}
public:
base-url: ${SKILLHUB_PUBLIC_BASE_URL:}
access-policy:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static org.mockito.Mockito.mock;

import com.iflytek.skillhub.auth.bootstrap.PassiveSessionAuthenticator;
import com.iflytek.skillhub.auth.cas.CasProperties;
import com.iflytek.skillhub.auth.direct.DirectAuthProvider;
import com.iflytek.skillhub.auth.direct.DirectAuthRequest;
import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
Expand Down Expand Up @@ -62,6 +63,7 @@ public Optional<PlatformPrincipal> authenticate(jakarta.servlet.http.HttpServlet
oauthProperties,
directAuthProperties,
bootstrapProperties,
new CasProperties(),
List.of(directProvider),
List.of(bootstrapProvider)
);
Expand Down Expand Up @@ -111,6 +113,7 @@ public Optional<PlatformPrincipal> authenticate(jakarta.servlet.http.HttpServlet
oauthProperties,
directAuthProperties,
bootstrapProperties,
new CasProperties(),
List.of(directProvider),
List.of(bootstrapProvider)
);
Expand All @@ -122,4 +125,52 @@ public Optional<PlatformPrincipal> authenticate(jakarta.servlet.http.HttpServlet
"bootstrap-private-sso:private-sso"
);
}

@Test
void listMethodsExposesCasWhenEnabled() {
OAuth2ClientProperties oauthProperties = new OAuth2ClientProperties();
DirectAuthProperties directAuthProperties = new DirectAuthProperties();
AuthSessionBootstrapProperties bootstrapProperties = new AuthSessionBootstrapProperties();

CasProperties casProperties = new CasProperties();
casProperties.setEnabled(true);
casProperties.setServerUrl("https://cas.example.com");
casProperties.setServiceUrl("https://skillhub.example.com/api/v1/auth/cas/callback");
casProperties.setProtocolVersion("3.0");
casProperties.setAllowInsecureServer(true);
casProperties.validate();

AuthMethodCatalog catalog = new AuthMethodCatalog(
oauthProperties,
directAuthProperties,
bootstrapProperties,
casProperties,
List.of(),
List.of()
);

assertThat(catalog.listMethods(null))
.extracting(method -> method.id() + ":" + method.methodType() + ":" + method.actionUrl())
.contains("cas:CAS_REDIRECT:/api/v1/auth/cas/login");
}

@Test
void listMethodsOmitsCasWhenDisabled() {
OAuth2ClientProperties oauthProperties = new OAuth2ClientProperties();
DirectAuthProperties directAuthProperties = new DirectAuthProperties();
AuthSessionBootstrapProperties bootstrapProperties = new AuthSessionBootstrapProperties();

AuthMethodCatalog catalog = new AuthMethodCatalog(
oauthProperties,
directAuthProperties,
bootstrapProperties,
new CasProperties(),
List.of(),
List.of()
);

assertThat(catalog.listMethods(null))
.extracting(method -> method.id())
.doesNotContain("cas");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.iflytek.skillhub.auth.cas;

import com.iflytek.skillhub.auth.identity.IdentityClaims;

import java.util.Map;

/**
* Adapts CAS ticket validation attributes to the platform-neutral IdentityClaims interface.
*/
public record CasIdentityClaims(
String subject,
String email,
String providerLogin,
Map<String, Object> extra
) implements IdentityClaims {

public static final String PROVIDER = "cas";

@Override
public String provider() {
return PROVIDER;
}

@Override
public boolean emailVerified() {
// CAS protocol does not verify email addresses — the attribute is passed through from the
// upstream directory (LDAP, AD, etc.) without cryptographic proof. Return false to prevent
// AccessPolicy implementations from trusting unverified claims.
return false;
}
}
Loading
Loading