chore: properly implement oidc

This commit is contained in:
Jeidnx 2024-11-12 14:59:22 +01:00
parent 868103cf73
commit 074e4bc136
No known key found for this signature in database
GPG Key ID: 0E9E697B7E99DF39
12 changed files with 228 additions and 107 deletions

View File

@ -18,7 +18,7 @@ dependencies {
implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1' implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1'
implementation 'com.github.FireMasterK.NewPipeExtractor:NewPipeExtractor:a64e202bb498032e817a702145263590829f3c1d' implementation 'com.github.FireMasterK.NewPipeExtractor:NewPipeExtractor:a64e202bb498032e817a702145263590829f3c1d'
implementation 'com.github.FireMasterK:nanojson:9f4af3b739cc13f3d0d9d4b758bbe2b2ae7119d7' implementation 'com.github.FireMasterK:nanojson:9f4af3b739cc13f3d0d9d4b758bbe2b2ae7119d7'
implementation 'com.nimbusds:oauth2-oidc-sdk:11.5' implementation 'com.nimbusds:oauth2-oidc-sdk:11.20.1'
implementation 'com.fasterxml.jackson.core:jackson-core:2.17.2' implementation 'com.fasterxml.jackson.core:jackson-core:2.17.2'
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.17.2' implementation 'com.fasterxml.jackson.core:jackson-annotations:2.17.2'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.2' implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.2'

View File

@ -90,10 +90,10 @@ hibernate.connection.password:changeme
#frontend.statusPageUrl:https://kavin.rocks #frontend.statusPageUrl:https://kavin.rocks
#frontend.donationUrl:https://kavin.rocks #frontend.donationUrl:https://kavin.rocks
# Oidc configuration # SSO via OIDC
#oidc.provider.INSERT_HERE.name:INSERT_HERE # each provider needs to have these three options specified. <NAME> is the
#oidc.provider.INSERT_HERE.clientId:INSERT_HERE # friendly name which will be shown to the clients and used in the database.
#oidc.provider.INSERT_HERE.clientSecret:INSERT_HERE # If you want to change the name later, you will have to update the database.
#oidc.provider.INSERT_HERE.authUri:INSERT_HERE # oidc.provider.<NAME>.clientId:<Client_id>
#oidc.provider.INSERT_HERE.tokenUri:INSERT_HERE # oidc.provider.<NAME>.clientSecret:<Client_secret>
#oidc.provider.INSERT_HERE.userinfoUri:INSERT_HERE # oidc.provider.<NAME>.issuer:<Issuer_url>

View File

@ -196,17 +196,13 @@ public class Constants {
} }
}); });
oidcProviderConfig.forEach((provider, config) -> { oidcProviderConfig.forEach((provider, config) -> {
ObjectNode providerNode = frontendProperties.putObject(provider);
OIDC_PROVIDERS.add(new OidcProvider( OIDC_PROVIDERS.add(new OidcProvider(
getRequiredMapValue(config, "name"), provider,
getRequiredMapValue(config, "clientId"), getRequiredMapValue(config, "clientId"),
getRequiredMapValue(config, "clientSecret"), getRequiredMapValue(config, "clientSecret"),
getRequiredMapValue(config, "authUri"), getRequiredMapValue(config, "issuer")
getRequiredMapValue(config, "tokenUri"),
getRequiredMapValue(config, "userinfoUri")
)); ));
providerNames.add(provider); providerNames.add(provider);
config.forEach(providerNode::put);
}); });
frontendProperties.put("imageProxyUrl", IMAGE_PROXY_PART); frontendProperties.put("imageProxyUrl", IMAGE_PROXY_PART);
frontendProperties.putArray("countries").addAll( frontendProperties.putArray("countries").addAll(

View File

@ -274,7 +274,7 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
return switch (function) { return switch (function) {
case "login" -> UserHandlers.oidcLoginResponse(provider, request.getQueryParameter("redirect")); case "login" -> UserHandlers.oidcLoginResponse(provider, request.getQueryParameter("redirect"));
case "callback" -> UserHandlers.oidcCallbackResponse(provider, URI.create(request.getFullUrl())); case "callback" -> UserHandlers.oidcCallbackResponse(provider, URI.create(request.getFullUrl()));
case "delete" -> UserHandlers.oidcDeleteResponse(provider, URI.create(request.getFullUrl())); case "delete" -> UserHandlers.oidcDeleteCallback(provider, URI.create(request.getFullUrl()));
default -> HttpResponse.ofCode(500).withHtml("Invalid function `" + function + "`"); default -> HttpResponse.ofCode(500).withHtml("Invalid function `" + function + "`");
}; };
} catch (Exception e) { } catch (Exception e) {
@ -491,6 +491,13 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
} catch (Exception e) { } catch (Exception e) {
return getErrorResponse(e, request.getPath()); return getErrorResponse(e, request.getPath());
} }
})).map(GET, "/user/delete", AsyncServlet.ofBlocking(executor, request -> {
try {
var session = request.getQueryParameter("session");
return UserHandlers.oidcDeleteRequest(session);
} catch (Exception e) {
return getErrorResponse(e, request.getPath());
}
})).map(POST, "/logout", AsyncServlet.ofBlocking(executor, request -> { })).map(POST, "/logout", AsyncServlet.ofBlocking(executor, request -> {
try { try {
return getJsonResponse(UserHandlers.logoutResponse(request.getHeader(AUTHORIZATION)), "private"); return getJsonResponse(UserHandlers.logoutResponse(request.getHeader(AUTHORIZATION)), "private");

View File

@ -1,12 +1,18 @@
package me.kavin.piped.server.handlers.auth; package me.kavin.piped.server.handlers.auth;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.proc.BadJOSEException;
import com.nimbusds.jwt.JWT;
import com.nimbusds.jwt.JWTParser;
import com.nimbusds.oauth2.sdk.*; import com.nimbusds.oauth2.sdk.*;
import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic; import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
import com.nimbusds.oauth2.sdk.id.State; import com.nimbusds.oauth2.sdk.id.State;
import com.nimbusds.oauth2.sdk.pkce.CodeChallengeMethod;
import com.nimbusds.oauth2.sdk.pkce.CodeVerifier;
import com.nimbusds.openid.connect.sdk.*; import com.nimbusds.openid.connect.sdk.*;
import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;
import com.nimbusds.openid.connect.sdk.claims.UserInfo; import com.nimbusds.openid.connect.sdk.claims.UserInfo;
import io.activej.http.HttpResponse; import io.activej.http.HttpResponse;
import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaBuilder;
@ -131,7 +137,8 @@ public class UserHandlers {
} }
URI callback = new URI(Constants.PUBLIC_URL + "/oidc/" + provider.name + "/callback"); URI callback = new URI(Constants.PUBLIC_URL + "/oidc/" + provider.name + "/callback");
OidcData data = new OidcData(redirectUri); CodeVerifier codeVerifier = new CodeVerifier();
OidcData data = new OidcData(redirectUri, codeVerifier);
String state = data.getState(); String state = data.getState();
PENDING_OIDC.put(state, data); PENDING_OIDC.put(state, data);
@ -139,8 +146,11 @@ public class UserHandlers {
AuthenticationRequest oidcRequest = new AuthenticationRequest.Builder( AuthenticationRequest oidcRequest = new AuthenticationRequest.Builder(
new ResponseType("code"), new ResponseType("code"),
new Scope("openid"), new Scope("openid"),
provider.clientID, callback).endpointURI(provider.authUri) provider.clientID, callback)
.state(new State(state)).nonce(data.nonce).build(); .endpointURI(provider.authUri)
.codeChallenge(codeVerifier, CodeChallengeMethod.S256)
.state(new State(state))
.nonce(data.nonce).build();
if (redirectUri.equals(Constants.FRONTEND_URL + "/login")) { if (redirectUri.equals(Constants.FRONTEND_URL + "/login")) {
return HttpResponse.redirect302(oidcRequest.toURI().toString()); return HttpResponse.redirect302(oidcRequest.toURI().toString());
@ -155,11 +165,9 @@ public class UserHandlers {
"\">here</a></body></html>"); "\">here</a></body></html>");
} }
public static HttpResponse oidcCallbackResponse(OidcProvider provider, URI requestUri) throws Exception { public static HttpResponse oidcCallbackResponse(OidcProvider provider, URI requestUri) throws Exception {
ClientAuthentication clientAuth = new ClientSecretBasic(provider.clientID, provider.clientSecret); AuthenticationSuccessResponse authResponse = parseOidcUri(requestUri);
AuthenticationSuccessResponse sr = parseOidcUri(requestUri); OidcData data = PENDING_OIDC.get(authResponse.getState().toString());
OidcData data = PENDING_OIDC.get(sr.getState().toString());
if (data == null) { if (data == null) {
return HttpResponse.ofCode(400).withHtml( return HttpResponse.ofCode(400).withHtml(
"Your oidc provider sent invalid state data. Try again or contact your oidc admin" "Your oidc provider sent invalid state data. Try again or contact your oidc admin"
@ -167,12 +175,15 @@ public class UserHandlers {
} }
URI callback = new URI(Constants.PUBLIC_URL + "/oidc/" + provider.name + "/callback"); URI callback = new URI(Constants.PUBLIC_URL + "/oidc/" + provider.name + "/callback");
AuthorizationCode code = sr.getAuthorizationCode(); AuthorizationCode code = authResponse.getAuthorizationCode();
AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback);
AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback, data.pkceVerifier);
TokenRequest tokenReq = new TokenRequest(provider.tokenUri, clientAuth, codeGrant); ClientAuthentication clientAuth = new ClientSecretBasic(provider.clientID, provider.clientSecret);
OIDCTokenResponse tokenResponse = (OIDCTokenResponse) OIDCTokenResponseParser.parse(tokenReq.toHTTPRequest().send()); TokenRequest tokenReq = new TokenRequest.Builder(provider.tokenUri, clientAuth, codeGrant).build();
com.nimbusds.oauth2.sdk.http.HTTPResponse tokenResponseText = tokenReq.toHTTPRequest().send();
OIDCTokenResponse tokenResponse = (OIDCTokenResponse) OIDCTokenResponseParser.parse(tokenResponseText);
if (!tokenResponse.indicatesSuccess()) { if (!tokenResponse.indicatesSuccess()) {
TokenErrorResponse errorResponse = tokenResponse.toErrorResponse(); TokenErrorResponse errorResponse = tokenResponse.toErrorResponse();
@ -181,11 +192,17 @@ public class UserHandlers {
OIDCTokenResponse successResponse = tokenResponse.toSuccessResponse(); OIDCTokenResponse successResponse = tokenResponse.toSuccessResponse();
if (data.isInvalidNonce((String) successResponse.getOIDCTokens().getIDToken().getJWTClaimsSet().getClaim("nonce"))) { JWT idToken = JWTParser.parse(successResponse.getOIDCTokens().getIDTokenString());
return HttpResponse.ofCode(400).withHtml(
"Your oidc provider sent an invalid nonce. Try again or contact your oidc admin" try {
); provider.validator.validate(idToken, data.nonce);
} } catch (BadJOSEException e) {
System.out.println("Invalid token received: " + e.toString());
return HttpResponse.ofCode(400).withHtml("Received a bad token. Please try again");
} catch (JOSEException e) {
System.out.println("Token processing error" + e.toString());
return HttpResponse.ofCode(500).withHtml("Internal processing error. Please try again");
}
UserInfoRequest ur = new UserInfoRequest(provider.userinfoUri, successResponse.getOIDCTokens().getBearerAccessToken()); UserInfoRequest ur = new UserInfoRequest(provider.userinfoUri, successResponse.getOIDCTokens().getBearerAccessToken());
UserInfoResponse userInfoResponse = UserInfoResponse.parse(ur.toHTTPRequest().send()); UserInfoResponse userInfoResponse = UserInfoResponse.parse(ur.toHTTPRequest().send());
@ -200,38 +217,86 @@ public class UserHandlers {
UserInfo userInfo = userInfoResponse.toSuccessResponse().getUserInfo(); UserInfo userInfo = userInfoResponse.toSuccessResponse().getUserInfo();
String sub = userInfo.getSubject().toString();
String uid = userInfo.getSubject().toString();
String sessionId; String sessionId;
try (Session s = DatabaseSessionFactory.createSession()) { try (Session s = DatabaseSessionFactory.createSession()) {
// TODO: Add oidc provider to database
String dbName = provider + "-" + uid;
CriteriaBuilder cb = s.getCriteriaBuilder(); CriteriaBuilder cb = s.getCriteriaBuilder();
CriteriaQuery<User> cr = cb.createQuery(User.class); CriteriaQuery<OidcUserData> cr = cb.createQuery(OidcUserData.class);
Root<User> root = cr.from(User.class); Root<OidcUserData> root = cr.from(OidcUserData.class);
cr.select(root).where(root.get("username").in(
dbName
));
User dbuser = s.createQuery(cr).uniqueResult(); cr.select(root).where(root.get("sub").in(sub));
if (dbuser == null) { OidcUserData dbuser = s.createQuery(cr).uniqueResult();
User newuser = new User(dbName, "", Set.of());
if (dbuser != null) {
sessionId = dbuser.getUser().getSessionId();
} else {
String username = userInfo.getPreferredUsername();
OidcUserData newUser = new OidcUserData(sub, username, provider.name);
var tr = s.beginTransaction(); var tr = s.beginTransaction();
s.persist(newuser); s.persist(newUser);
tr.commit(); tr.commit();
sessionId = newUser.getUser().getSessionId();
sessionId = newuser.getSessionId(); }
} else sessionId = dbuser.getSessionId();
} }
return HttpResponse.redirect302(data.data + "?session=" + sessionId); return HttpResponse.redirect302(data.data + "?session=" + sessionId);
} }
public static HttpResponse oidcDeleteResponse(OidcProvider provider, URI requestUri) throws Exception { public static HttpResponse oidcDeleteRequest(String session) throws Exception {
ClientAuthentication clientAuth = new ClientSecretBasic(provider.clientID, provider.clientSecret);
if (StringUtils.isBlank(session)) {
return HttpResponse.ofCode(400).withHtml("session is a required parameter");
}
OidcProvider provider = null;
try (Session s = DatabaseSessionFactory.createSession()) {
User user = DatabaseHelper.getUserFromSession(session);
if (user == null) {
return HttpResponse.ofCode(400).withHtml("User not found");
}
CriteriaBuilder cb = s.getCriteriaBuilder();
CriteriaQuery<OidcUserData> cr = cb.createQuery(OidcUserData.class);
Root<OidcUserData> root = cr.from(OidcUserData.class);
cr.select(root).where(cb.equal(root.get("user"), user));
OidcUserData oidcUserData = s.createQuery(cr).uniqueResult();
for (OidcProvider test: Constants.OIDC_PROVIDERS) {
if (test.name.equals(oidcUserData.getProvider())) {
provider = test;
}
}
}
if (provider == null) {
return HttpResponse.ofCode(400).withHtml("Invalid user");
}
CodeVerifier pkceVerifier = new CodeVerifier();
URI callback = URI.create(String.format("%s/oidc/%s/delete", Constants.PUBLIC_URL, provider.name));
OidcData data = new OidcData(session + "|" + Instant.now().getEpochSecond(), pkceVerifier);
String state = data.getState();
PENDING_OIDC.put(state, data);
AuthenticationRequest oidcRequest = new AuthenticationRequest.Builder(
new ResponseType("code"),
new Scope("openid"), provider.clientID, callback)
.endpointURI(provider.authUri)
.codeChallenge(pkceVerifier, CodeChallengeMethod.S256)
.state(new State(state))
.nonce(data.nonce)
// This parameter is optional and the idp does't have to honor it.
.maxAge(0)
.build();
return HttpResponse.redirect302(oidcRequest.toURI().toString());
}
public static HttpResponse oidcDeleteCallback(OidcProvider provider, URI requestUri) throws Exception {
AuthenticationSuccessResponse sr = parseOidcUri(requestUri); AuthenticationSuccessResponse sr = parseOidcUri(requestUri);
@ -247,10 +312,11 @@ public class UserHandlers {
URI callback = new URI(Constants.PUBLIC_URL + "/oidc/" + provider.name + "/delete"); URI callback = new URI(Constants.PUBLIC_URL + "/oidc/" + provider.name + "/delete");
AuthorizationCode code = sr.getAuthorizationCode(); AuthorizationCode code = sr.getAuthorizationCode();
AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback); AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback, data.pkceVerifier);
ClientAuthentication clientAuth = new ClientSecretBasic(provider.clientID, provider.clientSecret);
TokenRequest tokenRequest = new TokenRequest(provider.tokenUri, clientAuth, codeGrant); TokenRequest tokenRequest = new TokenRequest.Builder(provider.tokenUri, clientAuth, codeGrant).build();
TokenResponse tokenResponse = OIDCTokenResponseParser.parse(tokenRequest.toHTTPRequest().send()); TokenResponse tokenResponse = OIDCTokenResponseParser.parse(tokenRequest.toHTTPRequest().send());
if (!tokenResponse.indicatesSuccess()) { if (!tokenResponse.indicatesSuccess()) {
@ -260,16 +326,25 @@ public class UserHandlers {
OIDCTokenResponse successResponse = (OIDCTokenResponse) tokenResponse.toSuccessResponse(); OIDCTokenResponse successResponse = (OIDCTokenResponse) tokenResponse.toSuccessResponse();
JWTClaimsSet claims = successResponse.getOIDCTokens().getIDToken().getJWTClaimsSet(); JWT idToken = JWTParser.parse(successResponse.getOIDCTokens().getIDTokenString());
if (data.isInvalidNonce((String) claims.getClaim("nonce"))) { IDTokenClaimsSet claims;
return HttpResponse.ofCode(400).withHtml( try {
"Your oidc provider sent an invalid nonce. Please try again or contact your oidc admin." claims = provider.validator.validate(idToken, data.nonce);
); } catch (BadJOSEException e) {
System.out.println("Invalid token received: " + e.toString());
return HttpResponse.ofCode(400).withHtml("Received a bad token. Please try again");
} catch (JOSEException e) {
System.out.println("Token processing error" + e.toString());
return HttpResponse.ofCode(500).withHtml("Internal processing error. Please try again");
}
Long authTime = (Long) claims.getNumberClaim("auth_time");
if (authTime == null) {
return HttpResponse.ofCode(400).withHtml("Couldn't get the `auth_time` claim from the provided id token");
} }
long authTime = (long) claims.getClaim("auth_time");
if (authTime < start) { if (authTime < start) {
return HttpResponse.ofCode(500).withHtml( return HttpResponse.ofCode(500).withHtml(
"Your oidc provider didn't verify your identity. Please try again or contact your oidc admin." "Your oidc provider didn't verify your identity. Please try again or contact your oidc admin."
@ -277,7 +352,6 @@ public class UserHandlers {
} }
try (Session s = DatabaseSessionFactory.createSession()) { try (Session s = DatabaseSessionFactory.createSession()) {
var tr = s.beginTransaction(); var tr = s.beginTransaction();
s.remove(DatabaseHelper.getUserFromSession(session)); s.remove(DatabaseHelper.getUserFromSession(session));
tr.commit(); tr.commit();
@ -297,31 +371,6 @@ public class UserHandlers {
String hash = user.getPassword(); String hash = user.getPassword();
if (hash.isEmpty()) {
CriteriaBuilder cb = s.getCriteriaBuilder();
CriteriaQuery<OidcUserData> cr = cb.createQuery(OidcUserData.class);
Root<OidcUserData> root = cr.from(OidcUserData.class);
cr.select(root).where(cb.equal(root.get("user"), user.getId()));
OidcUserData oidcUserData = s.createQuery(cr).uniqueResult();
//TODO: Get user from oidc table and lookup provider
OidcProvider provider = Constants.OIDC_PROVIDERS.get(0);
URI callback = URI.create(String.format("%s/oidc/%s/delete", Constants.PUBLIC_URL, provider.name));
OidcData data = new OidcData(session + "|" + Instant.now().getEpochSecond());
String state = data.getState();
PENDING_OIDC.put(state, data);
AuthenticationRequest oidcRequest = new AuthenticationRequest.Builder(
new ResponseType("code"),
new Scope("openid"), provider.clientID, callback).endpointURI(provider.authUri)
.state(new State(state)).nonce(data.nonce).maxAge(0).build();
return mapper.writeValueAsBytes(mapper.createObjectNode()
.put("redirect", oidcRequest.toURI().toString()));
}
if (!hashMatch(hash, pass)) if (!hashMatch(hash, pass))
ExceptionHandler.throwErrorResponse(new IncorrectCredentialsResponse()); ExceptionHandler.throwErrorResponse(new IncorrectCredentialsResponse());
@ -333,7 +382,6 @@ public class UserHandlers {
} }
} }
public static byte[] logoutResponse(String session) throws JsonProcessingException { public static byte[] logoutResponse(String session) throws JsonProcessingException {
if (StringUtils.isBlank(session)) if (StringUtils.isBlank(session))

View File

@ -20,7 +20,7 @@ public class DatabaseSessionFactory {
sessionFactory = configuration.addAnnotatedClass(User.class).addAnnotatedClass(Channel.class) sessionFactory = configuration.addAnnotatedClass(User.class).addAnnotatedClass(Channel.class)
.addAnnotatedClass(Video.class).addAnnotatedClass(PubSub.class).addAnnotatedClass(Playlist.class) .addAnnotatedClass(Video.class).addAnnotatedClass(PubSub.class).addAnnotatedClass(Playlist.class)
.addAnnotatedClass(PlaylistVideo.class).addAnnotatedClass(UnauthenticatedSubscription.class).buildSessionFactory(); .addAnnotatedClass(PlaylistVideo.class).addAnnotatedClass(UnauthenticatedSubscription.class).addAnnotatedClass(OidcUserData.class).buildSessionFactory();
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }

View File

@ -1,6 +1,8 @@
package me.kavin.piped.utils.obj; package me.kavin.piped.utils.obj;
import com.nimbusds.oauth2.sdk.pkce.CodeVerifier;
import com.nimbusds.openid.connect.sdk.Nonce; import com.nimbusds.openid.connect.sdk.Nonce;
import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.MessageDigest; import java.security.MessageDigest;
@ -9,16 +11,17 @@ import java.util.Base64;
public class OidcData { public class OidcData {
public final Nonce nonce; public final Nonce nonce;
public final CodeVerifier pkceVerifier;
public final String data;
public String data; public OidcData(String data, CodeVerifier pkceVerifier) {
public OidcData(String data) {
this.nonce = new Nonce(); this.nonce = new Nonce();
this.pkceVerifier = pkceVerifier;
this.data = data; this.data = data;
} }
public boolean isInvalidNonce(String nonce) { public boolean validateNonce(String nonce) {
return !nonce.equals(this.nonce.toString()); return this.nonce.toString().equals(nonce);
} }
public String getState() { public String getState() {

View File

@ -2,28 +2,35 @@ package me.kavin.piped.utils.obj;
import com.nimbusds.oauth2.sdk.auth.Secret; import com.nimbusds.oauth2.sdk.auth.Secret;
import com.nimbusds.oauth2.sdk.id.ClientID; import com.nimbusds.oauth2.sdk.id.ClientID;
import com.nimbusds.oauth2.sdk.id.Issuer;
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException;
public class OidcProvider { public class OidcProvider {
public String name; public final String name;
public ClientID clientID; public final ClientID clientID;
public Secret clientSecret; public final Secret clientSecret;
public URI authUri; public URI authUri;
public URI tokenUri; public URI tokenUri;
public URI userinfoUri; public URI userinfoUri;
public IDTokenValidator validator;
public OidcProvider(String name, String clientID, String clientSecret, String authUri, String tokenUri, String userinfoUri) { public OidcProvider(String name, String clientId, String clientSecret, String issuer){
this.name = name; this.name = name;
this.clientID = new ClientID(clientID); this.clientID = new ClientID(clientId);
this.clientSecret = new Secret(clientSecret); this.clientSecret = new Secret(clientSecret);
try { try {
this.authUri = new URI(authUri); Issuer iss = new Issuer(issuer);
this.tokenUri = new URI(tokenUri); OIDCProviderMetadata providerData = OIDCProviderMetadata.resolve(iss);
this.userinfoUri = new URI(userinfoUri); this.authUri = providerData.getAuthorizationEndpointURI();
} catch (URISyntaxException e) { this.tokenUri = providerData.getTokenEndpointURI();
System.err.println("Malformed URI for oidc provider '" + name + "' found."); this.userinfoUri = providerData.getUserInfoEndpointURI();
this.validator = new IDTokenValidator(iss, this.clientID, providerData.getIDTokenJWSAlgs().getFirst(), providerData.getJWKSetURI().toURL());
} catch (Exception e ) {
System.err.println("Failed to get configuration for '" + name + "': " + e);
System.exit(1); System.exit(1);
} }
} }

View File

@ -1,19 +1,57 @@
package me.kavin.piped.utils.obj.db; package me.kavin.piped.utils.obj.db;
import java.util.Set;
import org.hibernate.annotations.Cascade;
import java.io.Serializable;
import jakarta.persistence.*; import jakarta.persistence.*;
@Entity @Entity
@Table(name = "oidc_user_data") @Table(name = "oidc_user_data")
public class OidcUserData { public class OidcUserData implements Serializable {
@Column(unique = true) public OidcUserData() {
}
public OidcUserData(String sub, String username, String provider) {
this.sub = sub;
this.provider = provider;
this.user = new User(username,"", Set.of());
}
@Column(name = "sub", unique = true, length = 255)
@Id @Id
private String sub; private String sub;
@OneToOne @OneToOne
@Cascade(org.hibernate.annotations.CascadeType.ALL)
private User user; private User user;
@Column(name = "provider", nullable = false)
private String provider; private String provider;
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public String getSub() {
return sub;
}
public void setSub(String sub) {
this.sub = sub;
}
public String getProvider() {
return provider;
}
public void setProvider(String provider) {
this.provider = provider;
}
} }

View File

@ -21,7 +21,7 @@ public class User implements Serializable {
@Column(name = "id") @Column(name = "id")
private long id; private long id;
@Column(name = "username", unique = true, length = 32) @Column(name = "username", unique = true, length = 24)
private String username; private String username;
@Column(name = "password", columnDefinition = "text") @Column(name = "password", columnDefinition = "text")

View File

@ -6,4 +6,5 @@
<include file="version/0-init.xml" relativeToChangelogFile="true"/> <include file="version/0-init.xml" relativeToChangelogFile="true"/>
<include file="version/1-fix-subs.xml" relativeToChangelogFile="true"/> <include file="version/1-fix-subs.xml" relativeToChangelogFile="true"/>
<include file="version/2-add-oidc.xml" relativeToChangelogFile="true"/>
</databaseChangeLog> </databaseChangeLog>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet id="create-playlist-oidc-data" author="jeidnx">
<createTable tableName="oidc_user_data">
<column name="sub" type="VARCHAR(255)">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="provider" type="VARCHAR(50)">
<constraints nullable="false"/>
</column>
<column name="user_id" type="BIGINT">
<constraints nullable="false" referencedColumnNames="id" referencedTableName="users" foreignKeyName="id"/>
</column>
</createTable>
</changeSet>
</databaseChangeLog>