-
Notifications
You must be signed in to change notification settings - Fork 248
Added OAUTH authentication to backend #1859
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # Example jwks file for the keys that are needed for Open ID Connect | ||
| { | ||
| "keys": [ | ||
| { | ||
| "kid": "3VcAf_wFO6KQz4RiKowja6IW35QJ40RkXSBgkgcfTfw", | ||
| "kty": "RSA", | ||
| "alg": "RS256", | ||
| "use": "sig", | ||
| "n": "qvuxqeloNtBwAwIOlfu48bd9-VnELl2D0DdGfUGKh_0_5gFgbXiGytGG11a_IC6qqlmmIWU4xuy-2Q2uytrQAkrZMTPmsT88ZrT84HCMUlxgqU5QWUPRGmlwGDuuPNXyeYDPbEtX9du8PQb6DNuu2kWMLmm_xjYwQzIzMPxR49xsR9h0N-wMHwc-fmSgkR02Io96I1NkQX3DHCuVvPFBp5cUhfb5lXwHGe1cdx3D4koA0y0NJ1EsOjfuMDv4AUtZFqqUKDEbg-ADoYA4HtfHOjcciMYXkEbb5FejlVCsppF_HMWuFtNqF6_V0FOfKvmNnJ0WzUD9NR6BJ_VqDxGNGQ", | ||
| "e": "AQAB", | ||
| "x5c": [ | ||
| "MIICmzCCAYMCBgGbjmGlkjANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjYwMTA1MTMzNzAyWhcNMzYwMTA1MTMzODQyWjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCq+7Gp6Wg20HADAg6V+7jxt335WcQuXYPQN0Z9QYqH/T/mAWBteIbK0YbXVr8gLqqqWaYhZTjG7L7ZDa7K2tACStkxM+axPzxmtPzgcIxSXGCpTlBZQ9EaaXAYO6481fJ5gM9sS1f127w9BvoM267aRYwuab/GNjBDMjMw/FHj3GxH2HQ37AwfBz5+ZKCRHTYij3ojU2RBfcMcK5W88UGnlxSF9vmVfAcZ7Vx3HcPiSgDTLQ0nUSw6N+4wO/gBS1kWqpQoMRuD4AOhgDge18c6NxyIxheQRtvkV6OVUKymkX8cxa4W02oXr9XQU58q+Y2cnRbNQP01HoEn9WoPEY0ZAgMBAAEwDQYJKoZIhvcNAQELBQADggEBABLaxd6HGIbKKd3hU56XbQHMcwbFTpPtL/P56JBUhcCVG1sCoLSE4csVW0o9Si1toKqh8hAtWDYA0/tm/IEjkwt3TcXdWjA/XoKGDbkz508aLDM2ni2CXq1p3wXwesCiGhkblg+liiNPyb2wU9RpmYRKkn16Qsb2qEw6AS1uaph0/+XLAPWENr5/pjJOgXQqok2VIOiAcrsnayE6zPDFQ2d2uYAKLOKNFgUKZ7K92DGH+qD8IheV8F6Wjs7cea7LZcgq0dlV85lmIQ8dZyQunL2QwGewIGVSShvT97vivk/xS86Zf9qQcaANAuvff/g1lCdxg2bFr8PewOXp3zQlqdU=" | ||
| ], | ||
| "x5t": "e0BNs1RTbxcrnk9Rnjs1n4pxcuY", | ||
| "x5t#S256": "wCTM8mdXANWTaYDj3w5cRm0yD5ybINNlULtxpAuA7gw" | ||
| }, | ||
| { | ||
| "kid": "UzDpZMBnNvfqtOmhny4gqZdjQLGWYuy2gAN3Wk_hm4Y", | ||
| "kty": "RSA", | ||
| "alg": "RSA-OAEP", | ||
| "use": "enc", | ||
| "n": "xGHOeXyy6B8_V1BsELJb5XWHfWJHS4w45oxvYbT--Dl6miixwrRizOCnpGz81JIPJZ_Dg-qGi372tQo10xegeg71h7GRMeCcGDA7QN7PXSLnwphkBQvV_uBzYnxDm98ZRLsBmyMnLRCEPdVxJX1_nxaqCk3-KbZxLVuEJM-AAMPlA0TcC5ZIB8pSWeYA_DhGswRb_t67GMEyXHKzNAvA_Bhc7E-FZ-C66WLH5e0bv47W2KSzCtJZNpRHGb-CdeJYzg7rR4M9PzGnCmtc1YEocWsgqHJ0lDO3Sl1g4XtIW84UPi2wBEvoBgQuT2UPhni9TqVd62yJ1F8F_MC4ZEIgpw", | ||
| "e": "AQAB", | ||
| "x5c": [ | ||
| "MIICmzCCAYMCBgGbjmGl7DANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjYwMTA1MTMzNzAyWhcNMzYwMTA1MTMzODQyWjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEYc55fLLoHz9XUGwQslvldYd9YkdLjDjmjG9htP74OXqaKLHCtGLM4KekbPzUkg8ln8OD6oaLfva1CjXTF6B6DvWHsZEx4JwYMDtA3s9dIufCmGQFC9X+4HNifEOb3xlEuwGbIyctEIQ91XElfX+fFqoKTf4ptnEtW4Qkz4AAw+UDRNwLlkgHylJZ5gD8OEazBFv+3rsYwTJccrM0C8D8GFzsT4Vn4LrpYsfl7Ru/jtbYpLMK0lk2lEcZv4J14ljODutHgz0/MacKa1zVgShxayCocnSUM7dKXWDhe0hbzhQ+LbAES+gGBC5PZQ+GeL1OpV3rbInUXwX8wLhkQiCnAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAMLa57gi0EgnVStdw6JGbjOYURhto8Gfahs/STDrpkmkMuvAh0bRzwi8yiJs75jX7ykBFNrAslsdnohMicXyjrHaMNbMwPeip9/XMISS7kR5sDqyz1AA+s28oyWAB9HWu5ntiD93LlJj+UU4qZ5+SpmKzRDs2MMhL8aWozuOwABGI4VrfvFRJL8O3J6ewxUeCikpEfB9UWkE+B+N/q8Wsn92n76z8UhqsdLOJVp1LwIuwOIcK9oCFZnSwfiGXSfK4e2QfxF6hWVAEdkQaKXsNZmxrqWE9CxdQp6ouOGaqiplzWUBDuWptNoaM57tLNo0jl0d6C1XPFUlzO9TfQHilEU=" | ||
| ], | ||
| "x5t": "HlsH_q2fqXiyZrxi4iWlbXoO51w", | ||
| "x5t#S256": "ByDXBjBIXVPzmIEts-GeqhlxhMQL1S2tM-8npsv2-jo" | ||
| } | ||
| ] | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -143,7 +143,7 @@ public function get($key): string { | |
| include(dirname(__FILE__) . '/../../inc/confv2.php'); | ||
|
|
||
| $decoder = new FirebaseDecoder( | ||
| new Secret($PEPPER[0], 'HS256') | ||
| new Secret($PEPPER[0], 'HS256', hash("sha256", $PEPPER[0])) | ||
|
Comment on lines
145
to
+146
|
||
| ); | ||
|
|
||
| $options = new Options( | ||
|
|
@@ -153,7 +153,7 @@ public function get($key): string { | |
| ); | ||
|
|
||
| $rules = [ | ||
| new RequestPathRule(ignore: ["/api/v2/auth/token", "/api/v2/helper/resetUserPassword", "/api/v2/openapi.json"]), | ||
| new RequestPathRule(ignore: ["/api/v2/auth/token", "/api/v2/auth/oauth-token", "/api/v2/helper/resetUserPassword", "/api/v2/openapi.json"]), | ||
| new RequestMethodRule(ignore: ["OPTIONS"]) | ||
| ]; | ||
| return new JwtAuthentication($options, $decoder, $rules); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -10,60 +10,124 @@ | |||||||||||||||||
| use DBA\QueryFilter; | ||||||||||||||||||
| use DBA\User; | ||||||||||||||||||
| use DBA\Factory; | ||||||||||||||||||
| use Firebase\JWT\JWK; | ||||||||||||||||||
|
|
||||||||||||||||||
| require_once(dirname(__FILE__) . "/../../load.php"); | ||||||||||||||||||
|
|
||||||||||||||||||
| function generateTokenForUser(Request $request, string $userName, int $expires) { | ||||||||||||||||||
| include(dirname(__FILE__) . '/../../confv2.php'); | ||||||||||||||||||
|
||||||||||||||||||
| $jti = bin2hex(random_bytes(16)); | ||||||||||||||||||
|
|
||||||||||||||||||
| $requested_scopes = $request->getParsedBody() ?: ["todo.all"]; | ||||||||||||||||||
|
|
||||||||||||||||||
| $valid_scopes = [ | ||||||||||||||||||
| "todo.create", | ||||||||||||||||||
| "todo.read", | ||||||||||||||||||
| "todo.update", | ||||||||||||||||||
| "todo.delete", | ||||||||||||||||||
| "todo.list", | ||||||||||||||||||
| "todo.all" | ||||||||||||||||||
| ]; | ||||||||||||||||||
|
|
||||||||||||||||||
| $scopes = array_filter($requested_scopes, function ($needle) use ($valid_scopes) { | ||||||||||||||||||
| return in_array($needle, $valid_scopes); | ||||||||||||||||||
| }); | ||||||||||||||||||
| // FIXME: This is duplicated and should be passed by HttpBasicMiddleware | ||||||||||||||||||
| $filter = new QueryFilter(User::USERNAME, $userName, "="); | ||||||||||||||||||
| $check = Factory::getUserFactory()->filter([Factory::FILTER => $filter]); | ||||||||||||||||||
| $user = $check[0]; | ||||||||||||||||||
|
Comment on lines
+36
to
+38
|
||||||||||||||||||
|
|
||||||||||||||||||
| if (empty($user)) { | ||||||||||||||||||
| throw new HttpError("No user with this userName in the database"); | ||||||||||||||||||
|
||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| $secret = $PEPPER[0]; | ||||||||||||||||||
| $now = new DateTime(); | ||||||||||||||||||
|
|
||||||||||||||||||
| $payload = [ | ||||||||||||||||||
| "iat" => $now->getTimeStamp(), | ||||||||||||||||||
| "exp" => $expires, | ||||||||||||||||||
| "jti" => $jti, | ||||||||||||||||||
| "userId" => $user->getId(), | ||||||||||||||||||
| "scope" => $scopes, | ||||||||||||||||||
| "iss" => "Hashtopolis", | ||||||||||||||||||
| "kid" => hash("sha256", $secret) | ||||||||||||||||||
| ]; | ||||||||||||||||||
|
|
||||||||||||||||||
| $token = JWT::encode($payload, $secret, "HS256"); | ||||||||||||||||||
|
|
||||||||||||||||||
| return $token; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| function extractBearerToken(Request $request): ?string { | ||||||||||||||||||
| $header = $request->getHeaderLine('Authorization'); | ||||||||||||||||||
|
|
||||||||||||||||||
| if (!$header) { | ||||||||||||||||||
| return null; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| if (!preg_match('/^Bearer\s+(.+)$/i', $header, $matches)) { | ||||||||||||||||||
| return null; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| return trim($matches[1]); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // Exchanges an oauth token for a application JWT token | ||||||||||||||||||
|
||||||||||||||||||
| $app->group("/api/v2/auth/oauth-token", function (RouteCollectorProxy $group) { | ||||||||||||||||||
|
|
||||||||||||||||||
| $group->post('', function (Request $request, Response $response, array $args): Response { | ||||||||||||||||||
| $jwks_file = file_get_contents("/keys/jwks.json"); | ||||||||||||||||||
|
||||||||||||||||||
| if (!$jwks_file) { | ||||||||||||||||||
| throw new HttpError("No jwks.json found, upload the jwks public keys to /keys/jwks.json to use OIDC authentication"); | ||||||||||||||||||
|
Comment on lines
+80
to
+82
|
||||||||||||||||||
| } | ||||||||||||||||||
| $jwks = json_decode($jwks_file, true); | ||||||||||||||||||
|
||||||||||||||||||
| $jwks = json_decode($jwks_file, true); | |
| $jwks = json_decode($jwks_file, true); | |
| if ($jwks === null && json_last_error() !== JSON_ERROR_NONE) { | |
| throw new HttpError("Invalid jwks.json: JSON decoding failed (" . json_last_error_msg() . ")"); | |
| } | |
| if (!is_array($jwks) || !isset($jwks['keys']) || !is_array($jwks['keys']) || empty($jwks['keys'])) { | |
| throw new HttpError("Invalid jwks.json format: expected a 'keys' array"); | |
| } |
Copilot
AI
Jan 14, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The json_decode() call does not check if the decoding was successful. If the jwks.json file contains invalid JSON, this will result in $jwks being null, which will then cause an error when passed to JWK::parseKeySet(). The code should check json_last_error() to handle malformed JSON gracefully.
Copilot
AI
Jan 14, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The preferred_username claim is accessed without checking if it exists in the decoded token. If the OAuth token does not contain this claim, this will cause an undefined property error. The code should validate that the claim exists before accessing it.
Copilot
AI
Jan 14, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment contains a typo: "This routes needs" should be "This route needs".
Copilot
AI
Jan 14, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is an extra set of parentheses around 'userId' causing a double parentheses issue. This should be $request->getAttribute('userId') instead of $request->getAttribute(('userId')).
Copilot
AI
Jan 14, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The response format for the "expires" field is inconsistent across authentication endpoints. The new OAuth endpoint (line 100) and updated /api/v2/auth/token endpoint (line 123) return a DateTime object, while the /api/v2/auth/refresh endpoint (line 161) still returns a timestamp via getTimeStamp(). For API consistency, all three endpoints should return the same format. Consider updating the refresh endpoint to also return a DateTime object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The file contains a comment on line 1 starting with '#', but JSON does not support comments. This will cause the JSON to be invalid. If comments are needed, this should be a separate documentation file, or the comment should be removed.