Skip to content

Commit

Permalink
0.2.0
Browse files Browse the repository at this point in the history
- Removed support for spatie's permissions
- Refactored class names
- Added config options
- Allows use without depending on database
  • Loading branch information
Hergen Dillema committed Sep 15, 2020
1 parent 3e5b57c commit 979fc2e
Show file tree
Hide file tree
Showing 13 changed files with 399 additions and 96 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "werk365/jwtauthroles",
"description": "Made to use fusionauth users in Laravel using JWT. Possible to either use pem keys directly or use the jwks endpoint.",
"version": "0.1.0",
"version": "0.2.0",
"license": "MIT",
"authors": [
{
Expand Down
39 changes: 21 additions & 18 deletions config/jwtAuthRoles.php
Original file line number Diff line number Diff line change
@@ -1,31 +1,34 @@
<?php

return [
'jwkUri' => env('JWKS_URL', 'http://localhost:9011/.well-known/jwks.json'),
'pemUri' => env('PEM_URL', 'http://localhost:9011/api/jwt/public-key'),

// Configure to use PEM endpoint (default) or JWK
'useJwk' => env('USE_JWK', true),
// If enabled, stores every user in the database
'useDB' => env('FA_USE_DB', false),

// Column name in the users table where uuid should be stored. Defaults to id but can be another column like 'uuid'
'userId' => env('FA_USR_ID', 'id'),

'autoCreateUser' => env('FA_CREATE_USR', true),

// This uses spatie's permissions package to give users the fusionauth roles.
// To use this, installing the permissions package is required
'usePermissions' => env('FA_USE_PERM', true),
// Creates a role if not found in database
'autoCreateRoles' => env('FA_CREATE_ROLES', true),
// Only if useDB = true
// Column name in the users table where uuid should be stored.'
'userId' => env('FA_USR_ID', 'uuid'),
// Only if useDB = true
'autoCreateUser' => env('FA_CREATE_USR', false),

'alg' => env('FA_ALG', 'RS256'),

// Allows you to skip validation, this is potentially dangerous,
// only use for testing or if the jwt has been validated by something like an api gateway
'validateJwt' => env('FA_VALIDATE', true),

// Only if validateJwt = true
'cache' => [
'enabled' => env('FA_CACHE_ENABLED', false),
'type' => env('FA_CACHE_TYPE', 'database'),
],

// Allows you to skip validation, this is potentially dangerous,
// only use for testing or if the jwt has been validated by something like an api gateway
'validateJwt' => env('FA_VALIDATE', false),
// Only if validateJwt = true
'jwkUri' => env('JWKS_URL', 'http://localhost:9011/.well-known/jwks.json'),
// Only if validateJwt = true
'pemUri' => env('PEM_URL', 'http://localhost:9011/api/jwt/public-key'),

// Only if validateJwt = true
// Configure to use PEM endpoint (default) or JWK
'useJwk' => env('USE_JWK', false),

];
34 changes: 34 additions & 0 deletions config/jwtauthroles.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

return [
// If enabled, stores every user in the database
'useDB' => env('FA_USE_DB', false),

// Only if useDB = true
// Column name in the users table where uuid should be stored.'
'userId' => env('FA_USR_ID', 'uuid'),
// Only if useDB = true
'autoCreateUser' => env('FA_CREATE_USR', false),

'alg' => env('FA_ALG', 'RS256'),

// Allows you to skip validation, this is potentially dangerous,
// only use for testing or if the jwt has been validated by something like an api gateway
'validateJwt' => env('FA_VALIDATE', true),

// Only if validateJwt = true
'cache' => [
'enabled' => env('FA_CACHE_ENABLED', false),
'type' => env('FA_CACHE_TYPE', 'database'),
],

// Only if validateJwt = true
'jwkUri' => env('JWKS_URL', 'http://localhost:9011/.well-known/jwks.json'),
// Only if validateJwt = true
'pemUri' => env('PEM_URL', 'http://localhost:9011/api/jwt/public-key'),

// Only if validateJwt = true
// Configure to use PEM endpoint (default) or JWK
'useJwk' => env('USE_JWK', false),

];
3 changes: 2 additions & 1 deletion database/migrations/create_jwtauth_tables.php.stub
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ class CreateJwtAuthTables extends Migration
Schema::create('jwt_users', function (Blueprint $table) {
$table->id();
$table->uuid('uuid');
$table->text('jwt');
$table->json('roles');
$table->json('claims');
$table->timestamps();
});
}
Expand Down
1 change: 0 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

Made to use fusionauth users in laravel using JWT. Possible to either use pem keys directly or use the jwks endpoint.

User roles from JWT can be converted to spatie/laravel-permissions roles automatically so the package can be used normally for permissions.

Even though it was made for fusionauth, should be quite general purpose for using JWTs/ jwks and roles in laravel.

Expand Down
15 changes: 15 additions & 0 deletions src/Exceptions/AuthException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace werk365\jwtauthroles\Exceptions;

use Symfony\Component\HttpKernel\Exception\HttpException;

class AuthException extends HttpException
{
public static function auth(int $status, string $message): self
{
$exception = new static($status, $message, null, []);

return $exception;
}
}
2 changes: 1 addition & 1 deletion src/Exceptions/authException.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use Symfony\Component\HttpKernel\Exception\HttpException;

class authException extends HttpException
class AuthException extends HttpException
{
public static function auth(int $status, string $message): self
{
Expand Down
18 changes: 0 additions & 18 deletions src/Facades/jwtAuthRoles.php

This file was deleted.

168 changes: 168 additions & 0 deletions src/JwtAuthRoles.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
<?php

namespace werk365\JwtAuthRoles;

use Firebase\JWT\JWT;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use phpseclib\Crypt\RSA;
use phpseclib\Math\BigInteger;
use werk365\jwtauthroles\Exceptions\AuthException;
use werk365\jwtauthroles\Models\JwtKey;
use werk365\jwtauthroles\Models\JwtUser;

class JwtAuthRoles
{
private static function getKid(string $jwt): ?string
{
if (Str::is('*.*.*', $jwt)) {
$header = JWT::jsonDecode(JWT::urlsafeB64Decode(Str::before($jwt, '.')));
if (isset($header->alg) && $header->alg !== config('jwtauthroles.alg')) {
throw AuthException::auth(422, 'Invalid algorithm');
}

return $header->kid ?? null;
} else {
throw AuthException::auth(422, 'Malformed JWT');
}
}

private static function getClaims(string $jwt): ?object
{
if (Str::is('*.*.*', $jwt)) {
$claims = explode('.', $jwt);
$claims = JWT::jsonDecode(JWT::urlsafeB64Decode($claims[1]));

return $claims ?? null;
} else {
throw AuthException::auth(422, 'Malformed JWT');
}
}

/**
* @param object $jwk
* @return bool|string|null
*/
private static function jwkToPem(object $jwk)
{
if (isset($jwk->e) && isset($jwk->n)) {
$rsa = new RSA();
$rsa->loadKey([
'e' => new BigInteger(JWT::urlsafeB64Decode($jwk->e), 256),
'n' => new BigInteger(JWT::urlsafeB64Decode($jwk->n), 256),
]);

return $rsa->getPublicKey();
}
throw AuthException::auth(500, 'Malformed jwk');
}

/**
* @param string $kid
* @param string $uri
* @return bool|string|null
*/
private static function getJwk(string $kid, string $uri)
{
$response = Http::get($uri);
$json = $response->getBody();
if ($json) {
$jwks = json_decode($json, false);
if ($jwks && isset($jwks->keys) && is_array($jwks->keys)) {
foreach ($jwks->keys as $jwk) {
if ($jwk->kid === $kid) {
return self::jwkToPem($jwk);
}
}
}
}
throw AuthException::auth(404, 'jwks endpoint not found');
}

private static function getPem(string $kid, string $uri): ?string
{
$response = Http::get($uri);
$json = $response->getBody();
if ($json) {
$pems = json_decode($json, false);
if ($pems && isset($pems->publicKeys) && is_object($pems->publicKeys)) {
foreach ($pems->publicKeys as $key=>$pem) {
if ($key === $kid) {
return $pem;
}
}
}
}
throw AuthException::auth(404, 'pem endpoint not found');
}

private static function verifyToken(string $jwt, string $uri, bool $jwk = false): object
{
$kid = self::getKid($jwt);
if (! $kid) {
throw AuthException::auth(422, 'Malformed JWT');
}
if (config('jwtauthroles.cache.enabled')) {
if (config('jwtauthroles.cache.type') === 'database') {
$row = JwtKey::where('kid', $kid)
->orderBy('created_at', 'desc')
->first('key');
}
}

$publicKey = $row->key
?? $jwk
? self::getJwk($kid, $uri)
: self::getPem($kid, $uri);

if (! isset($publicKey) || ! $publicKey) {
throw AuthException::auth(500, 'Unable to validate JWT');
}

if (config('jwtauthroles.cache.enabled')) {
if (config('jwtauthroles.cache.type') === 'database') {
$row = $row ?? JwtKey::create(['kid' => $kid, 'key' => $publicKey]);
}
}

return JWT::decode($jwt, $publicKey, [config('jwtauthroles.alg')]);
}

/** @return mixed */
public static function authUser(object $request)
{
$jwt = $request->bearerToken();

$uri = config('jwtauthroles.useJwk')
? config('jwtauthroles.jwkUri')
: config('jwtauthroles.pemUri');

if (! config('jwtauthroles.validateJwt')) {
$claims = self::getClaims($jwt);
} else {
$claims = self::verifyToken($jwt, $uri, config('jwtauthroles.useJwk'));
}

if(config('jwtauthroles.useDB')) {
if (config('jwtauthroles.autoCreateUser')) {
$user = JwtUser::firstOrNew([config('jwtauthroles.userId') => $claims->sub]);
$user[config('jwtauthroles.userId')] = $claims->sub;
$user->roles = json_encode($claims->roles);
$user->claims = json_encode($claims);
$user->save();
} else {
$user = JwtUser::where(config('jwtauthroles.userId'), '=', $claims->sub)->firstOrFail();
$user->roles = json_encode($claims->roles);
$user->claims = json_encode($claims);
$user->save();
}
} else {
$user = new JwtUser();
$user->uuid = $claims->sub;
$user->roles = $claims->roles;
$user->claims = $claims;
}

return $user;
}
}
Loading

0 comments on commit 979fc2e

Please sign in to comment.