Skip to content

Commit 8669079

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

File tree

12 files changed

+226
-105
lines changed

12 files changed

+226
-105
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: 118 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
package me.kavin.piped.server.handlers.auth;
22

33
import com.fasterxml.jackson.core.JsonProcessingException;
4-
import com.nimbusds.jwt.JWTClaimsSet;
4+
import com.nimbusds.jose.JOSEException;
5+
import com.nimbusds.jose.proc.BadJOSEException;
6+
import com.nimbusds.jwt.JWT;
7+
import com.nimbusds.jwt.JWTParser;
58
import com.nimbusds.oauth2.sdk.*;
69
import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
710
import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
811
import com.nimbusds.oauth2.sdk.id.State;
12+
import com.nimbusds.oauth2.sdk.pkce.CodeChallengeMethod;
13+
import com.nimbusds.oauth2.sdk.pkce.CodeVerifier;
914
import com.nimbusds.openid.connect.sdk.*;
15+
import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;
1016
import com.nimbusds.openid.connect.sdk.claims.UserInfo;
1117
import io.activej.http.HttpResponse;
1218
import jakarta.persistence.criteria.CriteriaBuilder;
@@ -131,16 +137,20 @@ public static HttpResponse oidcLoginResponse(OidcProvider provider, String redir
131137
}
132138

133139
URI callback = new URI(Constants.PUBLIC_URL + "/oidc/" + provider.name + "/callback");
134-
OidcData data = new OidcData(redirectUri);
140+
CodeVerifier codeVerifier = new CodeVerifier();
141+
OidcData data = new OidcData(redirectUri, codeVerifier);
135142
String state = data.getState();
136143

137144
PENDING_OIDC.put(state, data);
138145

139146
AuthenticationRequest oidcRequest = new AuthenticationRequest.Builder(
140147
new ResponseType("code"),
141148
new Scope("openid"),
142-
provider.clientID, callback).endpointURI(provider.authUri)
143-
.state(new State(state)).nonce(data.nonce).build();
149+
provider.clientID, callback)
150+
.endpointURI(provider.authUri)
151+
.codeChallenge(codeVerifier, CodeChallengeMethod.S256)
152+
.state(new State(state))
153+
.nonce(data.nonce).build();
144154

145155
if (redirectUri.equals(Constants.FRONTEND_URL + "/login")) {
146156
return HttpResponse.redirect302(oidcRequest.toURI().toString());
@@ -155,24 +165,25 @@ public static HttpResponse oidcLoginResponse(OidcProvider provider, String redir
155165
"\">here</a></body></html>");
156166
}
157167
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);
168+
AuthenticationSuccessResponse authResponse = parseOidcUri(requestUri);
161169

162-
OidcData data = PENDING_OIDC.get(sr.getState().toString());
170+
OidcData data = PENDING_OIDC.get(authResponse.getState().toString());
163171
if (data == null) {
164172
return HttpResponse.ofCode(400).withHtml(
165173
"Your oidc provider sent invalid state data. Try again or contact your oidc admin"
166174
);
167175
}
168176

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

180+
AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback, data.pkceVerifier);
173181

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

177188
if (!tokenResponse.indicatesSuccess()) {
178189
TokenErrorResponse errorResponse = tokenResponse.toErrorResponse();
@@ -181,11 +192,17 @@ public static HttpResponse oidcCallbackResponse(OidcProvider provider, URI reque
181192

182193
OIDCTokenResponse successResponse = tokenResponse.toSuccessResponse();
183194

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-
}
195+
JWT idToken = JWTParser.parse(successResponse.getOIDCTokens().getIDTokenString());
196+
197+
try {
198+
provider.validator.validate(idToken, data.nonce);
199+
} catch (BadJOSEException e) {
200+
System.out.println("Invalid token received: " + e.toString());
201+
return HttpResponse.ofCode(400).withHtml("Received a bad token. Please try again");
202+
} catch (JOSEException e) {
203+
System.out.println("Token processing error" + e.toString());
204+
return HttpResponse.ofCode(500).withHtml("Internal processing error. Please try again");
205+
}
189206

190207
UserInfoRequest ur = new UserInfoRequest(provider.userinfoUri, successResponse.getOIDCTokens().getBearerAccessToken());
191208
UserInfoResponse userInfoResponse = UserInfoResponse.parse(ur.toHTTPRequest().send());
@@ -200,38 +217,86 @@ public static HttpResponse oidcCallbackResponse(OidcProvider provider, URI reque
200217

201218
UserInfo userInfo = userInfoResponse.toSuccessResponse().getUserInfo();
202219

203-
204-
String uid = userInfo.getSubject().toString();
220+
String sub = userInfo.getSubject().toString();
205221
String sessionId;
206222
try (Session s = DatabaseSessionFactory.createSession()) {
207-
// TODO: Add oidc provider to database
208-
String dbName = provider + "-" + uid;
209223
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-
));
224+
CriteriaQuery<OidcUserData> cr = cb.createQuery(OidcUserData.class);
225+
Root<OidcUserData> root = cr.from(OidcUserData.class);
215226

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

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

221237
var tr = s.beginTransaction();
222-
s.persist(newuser);
238+
s.persist(newUser);
223239
tr.commit();
224240

225-
226-
sessionId = newuser.getSessionId();
227-
} else sessionId = dbuser.getSessionId();
241+
sessionId = newUser.getUser().getSessionId();
242+
}
228243
}
229244
return HttpResponse.redirect302(data.data + "?session=" + sessionId);
230-
231245
}
232246

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

236301
AuthenticationSuccessResponse sr = parseOidcUri(requestUri);
237302

@@ -247,8 +312,9 @@ public static HttpResponse oidcDeleteResponse(OidcProvider provider, URI request
247312

248313
URI callback = new URI(Constants.PUBLIC_URL + "/oidc/" + provider.name + "/delete");
249314
AuthorizationCode code = sr.getAuthorizationCode();
250-
AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback);
315+
AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback, data.pkceVerifier);
251316

317+
ClientAuthentication clientAuth = new ClientSecretBasic(provider.clientID, provider.clientSecret);
252318

253319
TokenRequest tokenRequest = new TokenRequest(provider.tokenUri, clientAuth, codeGrant);
254320
TokenResponse tokenResponse = OIDCTokenResponseParser.parse(tokenRequest.toHTTPRequest().send());
@@ -260,15 +326,24 @@ public static HttpResponse oidcDeleteResponse(OidcProvider provider, URI request
260326

261327
OIDCTokenResponse successResponse = (OIDCTokenResponse) tokenResponse.toSuccessResponse();
262328

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

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-
}
342+
Long authTime = (Long) claims.getNumberClaim("auth_time");
270343

271-
long authTime = (long) claims.getClaim("auth_time");
344+
if (authTime == null) {
345+
return HttpResponse.ofCode(400).withHtml("Couldn't get the `auth_time` claim from the provided id token");
346+
}
272347

273348
if (authTime < start) {
274349
return HttpResponse.ofCode(500).withHtml(
@@ -277,7 +352,6 @@ public static HttpResponse oidcDeleteResponse(OidcProvider provider, URI request
277352
}
278353

279354
try (Session s = DatabaseSessionFactory.createSession()) {
280-
281355
var tr = s.beginTransaction();
282356
s.remove(DatabaseHelper.getUserFromSession(session));
283357
tr.commit();
@@ -297,31 +371,6 @@ public static byte[] deleteUserResponse(String session, String pass) throws IOEx
297371

298372
String hash = user.getPassword();
299373

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-
}
325374
if (!hashMatch(hash, pass))
326375
ExceptionHandler.throwErrorResponse(new IncorrectCredentialsResponse());
327376

@@ -333,7 +382,6 @@ public static byte[] deleteUserResponse(String session, String pass) throws IOEx
333382
}
334383
}
335384

336-
337385
public static byte[] logoutResponse(String session) throws JsonProcessingException {
338386

339387
if (StringUtils.isBlank(session))

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)