diff --git a/docker/webapp/Dockerfile b/docker/webapp/Dockerfile --- a/docker/webapp/Dockerfile +++ b/docker/webapp/Dockerfile @@ -4,7 +4,7 @@ USER root -RUN dnf -y install findutils gnupg2 git rsync procps-ng +RUN dnf -y install findutils gnupg2 git rsync procps-ng php-sodium EXPOSE 8000 diff --git a/src/app/Http/Controllers/API/V4/VPNController.php b/src/app/Http/Controllers/API/V4/VPNController.php new file mode 100644 --- /dev/null +++ b/src/app/Http/Controllers/API/V4/VPNController.php @@ -0,0 +1,42 @@ +guard()->user(); + if (!$user) { + return response()->json(['status' => 'error', 'message' => "Invalid user"], 401); + } + + $signingKey = InMemory::plainText(\config("app.vpn.signing_key")); + + $tokenBuilder = (new Builder(new JoseEncoder(), ChainedFormatter::default())); + $token = $tokenBuilder + ->issuedAt(Carbon::now()->toImmutable()) + // The entitlement is hardcoded for now to default. + // Can be extended in the future based on user entitlements. + ->withClaim('entitlement', "default") + ->getToken(new Rsa\Sha256(), $signingKey); + + return response($token->toString()); + } +} diff --git a/src/composer.json b/src/composer.json --- a/src/composer.json +++ b/src/composer.json @@ -34,7 +34,8 @@ "sabre/vobject": "^4.5", "spatie/laravel-translatable": "^6.3", "spomky-labs/otphp": "~10.0.0", - "stripe/stripe-php": "^10.7" + "stripe/stripe-php": "^10.7", + "lcobucci/jwt": "^5.0" }, "require-dev": { "code-lts/doctum": "^5.5.1", diff --git a/src/routes/api.php b/src/routes/api.php --- a/src/routes/api.php +++ b/src/routes/api.php @@ -188,6 +188,8 @@ Route::post('support/request', [API\V4\SupportController::class, 'request']) ->withoutMiddleware(['auth:api', 'scope:api']) ->middleware(['api']); + + Route::get('vpn/token', [API\V4\VPNController::class, 'token']); } ); diff --git a/src/tests/Feature/Controller/VPNTest.php b/src/tests/Feature/Controller/VPNTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Feature/Controller/VPNTest.php @@ -0,0 +1,128 @@ + $privateKey]); + + $john = $this->getTestUser('john@kolab.org'); + + $response = $this->get("api/v4/vpn/token"); + $response->assertStatus(401); + + $response = $this->actingAs($john)->get("api/v4/vpn/token"); + $response->assertStatus(200); + + $jwt = $response->content(); + + $parser = new Parser(new JoseEncoder()); + $token = $parser->parse($jwt); + + $this->assertSame("default", $token->claims()->get('entitlement')); + $this->assertSame("2022-02-02T13:00:00+00:00", $token->claims()->get( + RegisteredClaims::ISSUED_AT + )->format(\DateTimeImmutable::RFC3339)); + $this->assertSame(0, Carbon::now()->diffInSeconds(new Carbon($token->claims()->get( + RegisteredClaims::ISSUED_AT + )))); + + $validator = new Validator(); + $key = InMemory::plainText($publicKey); + $validator->assert($token, new Constraint\SignedWith(new Sha256(), $key)); + + + $invalidKey = <<expectException(RequiredConstraintsViolated::class); + $key = InMemory::plainText($invalidKey); + $validator->assert($token, new Constraint\SignedWith(new Sha256(), $key)); + } +}