Skip to content

Commit e86c915

Browse files
author
Jeidnx
committed
chore: properly implement oidc
1 parent 868103c commit e86c915

File tree

11 files changed

+231
-101
lines changed

11 files changed

+231
-101
lines changed

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ dependencies {
1818
implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1'
1919
implementation 'com.github.FireMasterK.NewPipeExtractor:NewPipeExtractor:a64e202bb498032e817a702145263590829f3c1d'
2020
implementation 'com.github.FireMasterK:nanojson:9f4af3b739cc13f3d0d9d4b758bbe2b2ae7119d7'
21-
implementation 'com.nimbusds:oauth2-oidc-sdk:11.5'
21+
implementation 'com.nimbusds:oauth2-oidc-sdk:11.20.1'
2222
implementation 'com.fasterxml.jackson.core:jackson-core:2.17.2'
2323
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.17.2'
2424
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.2'

config.properties

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,10 @@ hibernate.connection.password:changeme
9090
#frontend.statusPageUrl:https://kavin.rocks
9191
#frontend.donationUrl:https://kavin.rocks
9292

93-
# Oidc configuration
94-
#oidc.provider.INSERT_HERE.name:INSERT_HERE
95-
#oidc.provider.INSERT_HERE.clientId:INSERT_HERE
96-
#oidc.provider.INSERT_HERE.clientSecret:INSERT_HERE
97-
#oidc.provider.INSERT_HERE.authUri:INSERT_HERE
98-
#oidc.provider.INSERT_HERE.tokenUri:INSERT_HERE
99-
#oidc.provider.INSERT_HERE.userinfoUri:INSERT_HERE
93+
# SSO via OIDC
94+
# each provider needs to have these three options specified. <NAME> is the
95+
# friendly name which will be shown to the clients and used in the database.
96+
# If you want to change the name later, you will have to update the database.
97+
# oidc.provider.<NAME>.clientId:<Client_id>
98+
# oidc.provider.<NAME>.clientSecret:<Client_secret>
99+
# oidc.provider.<NAME>.issuer:<Issuer_url>

src/main/java/me/kavin/piped/consts/Constants.java

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -196,17 +196,13 @@ else if (key.startsWith("oidc.provider")) {
196196
}
197197
});
198198
oidcProviderConfig.forEach((provider, config) -> {
199-
ObjectNode providerNode = frontendProperties.putObject(provider);
200199
OIDC_PROVIDERS.add(new OidcProvider(
201-
getRequiredMapValue(config, "name"),
200+
provider,
202201
getRequiredMapValue(config, "clientId"),
203202
getRequiredMapValue(config, "clientSecret"),
204-
getRequiredMapValue(config, "authUri"),
205-
getRequiredMapValue(config, "tokenUri"),
206-
getRequiredMapValue(config, "userinfoUri")
203+
getRequiredMapValue(config, "issuer")
207204
));
208205
providerNames.add(provider);
209-
config.forEach(providerNode::put);
210206
});
211207
frontendProperties.put("imageProxyUrl", IMAGE_PROXY_PART);
212208
frontendProperties.putArray("countries").addAll(

src/main/java/me/kavin/piped/server/ServerLauncher.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ AsyncServlet mainServlet(Executor executor) {
274274
return switch (function) {
275275
case "login" -> UserHandlers.oidcLoginResponse(provider, request.getQueryParameter("redirect"));
276276
case "callback" -> UserHandlers.oidcCallbackResponse(provider, URI.create(request.getFullUrl()));
277-
case "delete" -> UserHandlers.oidcDeleteResponse(provider, URI.create(request.getFullUrl()));
277+
case "delete" -> UserHandlers.oidcDeleteCallback(provider, URI.create(request.getFullUrl()));
278278
default -> HttpResponse.ofCode(500).withHtml("Invalid function `" + function + "`");
279279
};
280280
} catch (Exception e) {
@@ -491,6 +491,13 @@ AsyncServlet mainServlet(Executor executor) {
491491
} catch (Exception e) {
492492
return getErrorResponse(e, request.getPath());
493493
}
494+
})).map(GET, "/user/delete", AsyncServlet.ofBlocking(executor, request -> {
495+
try {
496+
var session = request.getQueryParameter("session");
497+
return UserHandlers.oidcDeleteRequest(session);
498+
} catch (Exception e) {
499+
return getErrorResponse(e, request.getPath());
500+
}
494501
})).map(POST, "/logout", AsyncServlet.ofBlocking(executor, request -> {
495502
try {
496503
return getJsonResponse(UserHandlers.logoutResponse(request.getHeader(AUTHORIZATION)), "private");

src/main/java/me/kavin/piped/server/handlers/auth/UserHandlers.java

Lines changed: 121 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
package me.kavin.piped.server.handlers.auth;
22

33
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.nimbusds.jose.JOSEException;
5+
import com.nimbusds.jose.proc.BadJOSEException;
6+
import com.nimbusds.jwt.JWT;
47
import com.nimbusds.jwt.JWTClaimsSet;
8+
import com.nimbusds.jwt.JWTParser;
59
import com.nimbusds.oauth2.sdk.*;
610
import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
711
import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
812
import com.nimbusds.oauth2.sdk.id.State;
13+
import com.nimbusds.oauth2.sdk.pkce.CodeChallengeMethod;
14+
import com.nimbusds.oauth2.sdk.pkce.CodeVerifier;
915
import com.nimbusds.openid.connect.sdk.*;
16+
import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;
1017
import com.nimbusds.openid.connect.sdk.claims.UserInfo;
1118
import io.activej.http.HttpResponse;
1219
import jakarta.persistence.criteria.CriteriaBuilder;
@@ -131,16 +138,20 @@ public static HttpResponse oidcLoginResponse(OidcProvider provider, String redir
131138
}
132139

133140
URI callback = new URI(Constants.PUBLIC_URL + "/oidc/" + provider.name + "/callback");
134-
OidcData data = new OidcData(redirectUri);
141+
CodeVerifier codeVerifier = new CodeVerifier();
142+
OidcData data = new OidcData(redirectUri, codeVerifier);
135143
String state = data.getState();
136144

137145
PENDING_OIDC.put(state, data);
138146

139147
AuthenticationRequest oidcRequest = new AuthenticationRequest.Builder(
140148
new ResponseType("code"),
141149
new Scope("openid"),
142-
provider.clientID, callback).endpointURI(provider.authUri)
143-
.state(new State(state)).nonce(data.nonce).build();
150+
provider.clientID, callback)
151+
.endpointURI(provider.authUri)
152+
.codeChallenge(codeVerifier, CodeChallengeMethod.S256)
153+
.state(new State(state))
154+
.nonce(data.nonce).build();
144155

145156
if (redirectUri.equals(Constants.FRONTEND_URL + "/login")) {
146157
return HttpResponse.redirect302(oidcRequest.toURI().toString());
@@ -155,24 +166,25 @@ public static HttpResponse oidcLoginResponse(OidcProvider provider, String redir
155166
"\">here</a></body></html>");
156167
}
157168
public static HttpResponse oidcCallbackResponse(OidcProvider provider, URI requestUri) throws Exception {
158-
ClientAuthentication clientAuth = new ClientSecretBasic(provider.clientID, provider.clientSecret);
159-
160-
AuthenticationSuccessResponse sr = parseOidcUri(requestUri);
169+
AuthenticationSuccessResponse authResponse = parseOidcUri(requestUri);
161170

162-
OidcData data = PENDING_OIDC.get(sr.getState().toString());
171+
OidcData data = PENDING_OIDC.get(authResponse.getState().toString());
163172
if (data == null) {
164173
return HttpResponse.ofCode(400).withHtml(
165174
"Your oidc provider sent invalid state data. Try again or contact your oidc admin"
166175
);
167176
}
168177

169178
URI callback = new URI(Constants.PUBLIC_URL + "/oidc/" + provider.name + "/callback");
170-
AuthorizationCode code = sr.getAuthorizationCode();
171-
AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback);
179+
AuthorizationCode code = authResponse.getAuthorizationCode();
172180

181+
AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback, data.pkceVerifier);
173182

183+
ClientAuthentication clientAuth = new ClientSecretBasic(provider.clientID, provider.clientSecret);
174184
TokenRequest tokenReq = new TokenRequest(provider.tokenUri, clientAuth, codeGrant);
175-
OIDCTokenResponse tokenResponse = (OIDCTokenResponse) OIDCTokenResponseParser.parse(tokenReq.toHTTPRequest().send());
185+
186+
com.nimbusds.oauth2.sdk.http.HTTPResponse tokenResponseText = tokenReq.toHTTPRequest().send();
187+
OIDCTokenResponse tokenResponse = (OIDCTokenResponse) OIDCTokenResponseParser.parse(tokenResponseText);
176188

177189
if (!tokenResponse.indicatesSuccess()) {
178190
TokenErrorResponse errorResponse = tokenResponse.toErrorResponse();
@@ -181,11 +193,17 @@ public static HttpResponse oidcCallbackResponse(OidcProvider provider, URI reque
181193

182194
OIDCTokenResponse successResponse = tokenResponse.toSuccessResponse();
183195

184-
if (data.isInvalidNonce((String) successResponse.getOIDCTokens().getIDToken().getJWTClaimsSet().getClaim("nonce"))) {
185-
return HttpResponse.ofCode(400).withHtml(
186-
"Your oidc provider sent an invalid nonce. Try again or contact your oidc admin"
187-
);
188-
}
196+
JWT idToken = JWTParser.parse(successResponse.getOIDCTokens().getIDTokenString());
197+
198+
try {
199+
provider.validator.validate(idToken, data.nonce);
200+
} catch (BadJOSEException e) {
201+
System.out.println("Invalid token received: " + e.toString());
202+
return HttpResponse.ofCode(400).withHtml("Received a bad token. Please try again");
203+
} catch (JOSEException e) {
204+
System.out.println("Token processing error" + e.toString());
205+
return HttpResponse.ofCode(500).withHtml("Internal processing error. Please try again");
206+
}
189207

190208
UserInfoRequest ur = new UserInfoRequest(provider.userinfoUri, successResponse.getOIDCTokens().getBearerAccessToken());
191209
UserInfoResponse userInfoResponse = UserInfoResponse.parse(ur.toHTTPRequest().send());
@@ -200,38 +218,87 @@ public static HttpResponse oidcCallbackResponse(OidcProvider provider, URI reque
200218

201219
UserInfo userInfo = userInfoResponse.toSuccessResponse().getUserInfo();
202220

203-
204-
String uid = userInfo.getSubject().toString();
221+
String sub = userInfo.getSubject().toString();
205222
String sessionId;
206223
try (Session s = DatabaseSessionFactory.createSession()) {
207-
// TODO: Add oidc provider to database
208-
String dbName = provider + "-" + uid;
209224
CriteriaBuilder cb = s.getCriteriaBuilder();
210-
CriteriaQuery<User> cr = cb.createQuery(User.class);
211-
Root<User> root = cr.from(User.class);
212-
cr.select(root).where(root.get("username").in(
213-
dbName
214-
));
225+
CriteriaQuery<OidcUserData> cr = cb.createQuery(OidcUserData.class);
226+
Root<OidcUserData> root = cr.from(OidcUserData.class);
215227

216-
User dbuser = s.createQuery(cr).uniqueResult();
228+
cr.select(root).where(root.get("sub").in(sub));
217229

218-
if (dbuser == null) {
219-
User newuser = new User(dbName, "", Set.of());
230+
OidcUserData dbuser = s.createQuery(cr).uniqueResult();
231+
232+
if (dbuser != null) {
233+
sessionId = dbuser.getUser().getSessionId();
234+
} else {
235+
String username = userInfo.getPreferredUsername();
236+
OidcUserData newUser = new OidcUserData(sub, username, provider.name);
220237

221238
var tr = s.beginTransaction();
222-
s.persist(newuser);
239+
s.persist(newUser);
223240
tr.commit();
224241

225-
226-
sessionId = newuser.getSessionId();
227-
} else sessionId = dbuser.getSessionId();
242+
sessionId = newUser.getUser().getSessionId();
243+
}
228244
}
229245
return HttpResponse.redirect302(data.data + "?session=" + sessionId);
230246

231247
}
232248

233-
public static HttpResponse oidcDeleteResponse(OidcProvider provider, URI requestUri) throws Exception {
234-
ClientAuthentication clientAuth = new ClientSecretBasic(provider.clientID, provider.clientSecret);
249+
public static HttpResponse oidcDeleteRequest(String session) throws Exception {
250+
251+
if (StringUtils.isBlank(session)) {
252+
return HttpResponse.ofCode(400).withHtml("session is a required parameter");
253+
}
254+
255+
OidcProvider provider = null;
256+
try (Session s = DatabaseSessionFactory.createSession()) {
257+
258+
User user = DatabaseHelper.getUserFromSession(session);
259+
260+
if (user == null) {
261+
return HttpResponse.ofCode(400).withHtml("User not found");
262+
}
263+
264+
CriteriaBuilder cb = s.getCriteriaBuilder();
265+
CriteriaQuery<OidcUserData> cr = cb.createQuery(OidcUserData.class);
266+
Root<OidcUserData> root = cr.from(OidcUserData.class);
267+
cr.select(root).where(cb.equal(root.get("user"), user));
268+
269+
OidcUserData oidcUserData = s.createQuery(cr).uniqueResult();
270+
271+
for (OidcProvider test: Constants.OIDC_PROVIDERS) {
272+
if (test.name.equals(oidcUserData.getProvider())) {
273+
provider = test;
274+
}
275+
}
276+
}
277+
278+
if (provider == null) {
279+
return HttpResponse.ofCode(400).withHtml("Invalid user");
280+
}
281+
CodeVerifier pkceVerifier = new CodeVerifier();
282+
283+
URI callback = URI.create(String.format("%s/oidc/%s/delete", Constants.PUBLIC_URL, provider.name));
284+
OidcData data = new OidcData(session + "|" + Instant.now().getEpochSecond(), pkceVerifier);
285+
String state = data.getState();
286+
PENDING_OIDC.put(state, data);
287+
288+
AuthenticationRequest oidcRequest = new AuthenticationRequest.Builder(
289+
new ResponseType("code"),
290+
new Scope("openid"), provider.clientID, callback)
291+
.endpointURI(provider.authUri)
292+
.codeChallenge(pkceVerifier, CodeChallengeMethod.S256)
293+
.state(new State(state))
294+
.nonce(data.nonce)
295+
// This parameter is optional and the idp does't have to honor it.
296+
.maxAge(0)
297+
.build();
298+
299+
return HttpResponse.redirect302(oidcRequest.toURI().toString());
300+
}
301+
public static HttpResponse oidcDeleteCallback(OidcProvider provider, URI requestUri) throws Exception {
235302

236303
AuthenticationSuccessResponse sr = parseOidcUri(requestUri);
237304

@@ -247,8 +314,10 @@ public static HttpResponse oidcDeleteResponse(OidcProvider provider, URI request
247314

248315
URI callback = new URI(Constants.PUBLIC_URL + "/oidc/" + provider.name + "/delete");
249316
AuthorizationCode code = sr.getAuthorizationCode();
250-
AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback);
317+
AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback, data.pkceVerifier);
318+
251319

320+
ClientAuthentication clientAuth = new ClientSecretBasic(provider.clientID, provider.clientSecret);
252321

253322
TokenRequest tokenRequest = new TokenRequest(provider.tokenUri, clientAuth, codeGrant);
254323
TokenResponse tokenResponse = OIDCTokenResponseParser.parse(tokenRequest.toHTTPRequest().send());
@@ -260,15 +329,26 @@ public static HttpResponse oidcDeleteResponse(OidcProvider provider, URI request
260329

261330
OIDCTokenResponse successResponse = (OIDCTokenResponse) tokenResponse.toSuccessResponse();
262331

263-
JWTClaimsSet claims = successResponse.getOIDCTokens().getIDToken().getJWTClaimsSet();
332+
JWT idToken = JWTParser.parse(successResponse.getOIDCTokens().getIDTokenString());
333+
334+
IDTokenClaimsSet claims;
335+
try {
336+
claims = provider.validator.validate(idToken, data.nonce);
337+
} catch (BadJOSEException e) {
338+
System.out.println("Invalid token received: " + e.toString());
339+
return HttpResponse.ofCode(400).withHtml("Received a bad token. Please try again");
340+
} catch (JOSEException e) {
341+
System.out.println("Token processing error" + e.toString());
342+
return HttpResponse.ofCode(500).withHtml("Internal processing error. Please try again");
343+
}
264344

265-
if (data.isInvalidNonce((String) claims.getClaim("nonce"))) {
266-
return HttpResponse.ofCode(400).withHtml(
267-
"Your oidc provider sent an invalid nonce. Please try again or contact your oidc admin."
268-
);
269-
}
270345

271-
long authTime = (long) claims.getClaim("auth_time");
346+
347+
Long authTime = (Long) claims.getNumberClaim("auth_time");
348+
349+
if (authTime == null) {
350+
return HttpResponse.ofCode(400).withHtml("Couldn't get the `auth_time` claim from the provided id token");
351+
}
272352

273353
if (authTime < start) {
274354
return HttpResponse.ofCode(500).withHtml(
@@ -277,7 +357,6 @@ public static HttpResponse oidcDeleteResponse(OidcProvider provider, URI request
277357
}
278358

279359
try (Session s = DatabaseSessionFactory.createSession()) {
280-
281360
var tr = s.beginTransaction();
282361
s.remove(DatabaseHelper.getUserFromSession(session));
283362
tr.commit();
@@ -297,31 +376,6 @@ public static byte[] deleteUserResponse(String session, String pass) throws IOEx
297376

298377
String hash = user.getPassword();
299378

300-
if (hash.isEmpty()) {
301-
302-
CriteriaBuilder cb = s.getCriteriaBuilder();
303-
CriteriaQuery<OidcUserData> cr = cb.createQuery(OidcUserData.class);
304-
Root<OidcUserData> root = cr.from(OidcUserData.class);
305-
cr.select(root).where(cb.equal(root.get("user"), user.getId()));
306-
307-
OidcUserData oidcUserData = s.createQuery(cr).uniqueResult();
308-
309-
//TODO: Get user from oidc table and lookup provider
310-
OidcProvider provider = Constants.OIDC_PROVIDERS.get(0);
311-
URI callback = URI.create(String.format("%s/oidc/%s/delete", Constants.PUBLIC_URL, provider.name));
312-
OidcData data = new OidcData(session + "|" + Instant.now().getEpochSecond());
313-
String state = data.getState();
314-
PENDING_OIDC.put(state, data);
315-
316-
AuthenticationRequest oidcRequest = new AuthenticationRequest.Builder(
317-
new ResponseType("code"),
318-
new Scope("openid"), provider.clientID, callback).endpointURI(provider.authUri)
319-
.state(new State(state)).nonce(data.nonce).maxAge(0).build();
320-
321-
322-
return mapper.writeValueAsBytes(mapper.createObjectNode()
323-
.put("redirect", oidcRequest.toURI().toString()));
324-
}
325379
if (!hashMatch(hash, pass))
326380
ExceptionHandler.throwErrorResponse(new IncorrectCredentialsResponse());
327381

src/main/java/me/kavin/piped/utils/DatabaseSessionFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public class DatabaseSessionFactory {
2020

2121
sessionFactory = configuration.addAnnotatedClass(User.class).addAnnotatedClass(Channel.class)
2222
.addAnnotatedClass(Video.class).addAnnotatedClass(PubSub.class).addAnnotatedClass(Playlist.class)
23-
.addAnnotatedClass(PlaylistVideo.class).addAnnotatedClass(UnauthenticatedSubscription.class).buildSessionFactory();
23+
.addAnnotatedClass(PlaylistVideo.class).addAnnotatedClass(UnauthenticatedSubscription.class).addAnnotatedClass(OidcUserData.class).buildSessionFactory();
2424
} catch (Exception e) {
2525
throw new RuntimeException(e);
2626
}

0 commit comments

Comments
 (0)