mirror of
https://github.com/TeamPiped/Piped-Backend.git
synced 2025-01-09 11:00:29 +05:30
chore: properly implement oidc
This commit is contained in:
parent
868103cf73
commit
074e4bc136
@ -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'
|
||||||
|
@ -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>
|
||||||
|
@ -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(
|
||||||
|
@ -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");
|
||||||
|
@ -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,10 +192,16 @@ 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());
|
||||||
@ -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,15 +326,24 @@ 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.getClaim("auth_time");
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
if (authTime < start) {
|
if (authTime < start) {
|
||||||
return HttpResponse.ofCode(500).withHtml(
|
return HttpResponse.ofCode(500).withHtml(
|
||||||
@ -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))
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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() {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
|
@ -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>
|
||||||
|
21
src/main/resources/changelog/version/2-add-oidc.xml
Normal file
21
src/main/resources/changelog/version/2-add-oidc.xml
Normal 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>
|
Loading…
Reference in New Issue
Block a user