diff --git a/config.demo/src/database/seeds/PassportSeeder.php b/config.demo/src/database/seeds/PassportSeeder.php --- a/config.demo/src/database/seeds/PassportSeeder.php +++ b/config.demo/src/database/seeds/PassportSeeder.php @@ -31,6 +31,21 @@ $client->id = \config('auth.proxy.client_id'); $client->save(); + // Create a client for Webmail SSO + $client = Passport::client()->forceFill([ + 'user_id' => null, + 'name' => 'Webmail SSO client', + 'secret' => \config('auth.sso.client_secret'), + 'provider' => 'users', + 'redirect' => 'https://' . \config('app.website_domain') . '/roundcubemail/index.php/login/oauth', + 'personal_access_client' => 0, + 'password_client' => 0, + 'revoked' => false, + 'allowed_scopes' => ['email', 'otp'], + ]); + $client->id = \config('auth.sso.client_id'); + $client->save(); + // Create a client for synapse oauth $client = Passport::client()->forceFill([ 'user_id' => null, diff --git a/src/app/Auth/IdentityEntity.php b/src/app/Auth/IdentityEntity.php --- a/src/app/Auth/IdentityEntity.php +++ b/src/app/Auth/IdentityEntity.php @@ -35,8 +35,16 @@ // TODO: Other claims // TODO: Should we use this in AuthController::oauthUserInfo() for some de-duplicaton? - return [ + $claims = [ 'email' => $this->user->email, ]; + + // Short living password for IMAP/SMTP + // We use same TTL as for the OAuth tokens, so clients can get a new password on token refresh + // TODO: We should create the password only when the access token scope requests it + $ttl = config('auth.token_expiry_minutes') * 60; + $claims['otp'] = Utils::tokenCreate((string) $this->user->id, $ttl); + + return $claims; } } diff --git a/src/app/Auth/Utils.php b/src/app/Auth/Utils.php --- a/src/app/Auth/Utils.php +++ b/src/app/Auth/Utils.php @@ -10,10 +10,11 @@ * Create a simple authentication token * * @param string $userid User identifier + * @param int $ttl Token's time to live (in seconds) * * @return string|null Encrypted token, Null on failure */ - public static function tokenCreate($userid): ?string + public static function tokenCreate($userid, $ttl = 10): ?string { // Note: Laravel's Crypt::encryptString() creates output that is too long // We need output string to be max. 127 characters. For that reason @@ -23,7 +24,7 @@ $key = config('app.key'); $iv = random_bytes(openssl_cipher_iv_length($cipher)); - $data = $userid . '!' . now()->addSeconds(10)->format('YmdHis'); + $data = $userid . '!' . now()->addSeconds($ttl)->format('YmdHis'); $value = openssl_encrypt($data, $cipher, $key, 0, $iv, $tag); diff --git a/src/app/Http/Controllers/API/AuthController.php b/src/app/Http/Controllers/API/AuthController.php --- a/src/app/Http/Controllers/API/AuthController.php +++ b/src/app/Http/Controllers/API/AuthController.php @@ -111,7 +111,10 @@ } try { - // league/oauth2-server/src/Grant/ code expects GET parameters, but we're using POST here + // OpenID handler reads parameters from the request query string (GET) + $request->query->replace($request->input()); + + // OAuth2 server's code also expects GET parameters, but we're using POST here $psrRequest = $psrRequest->withQueryParams($request->input()); $authRequest = $server->validateAuthorizationRequest($psrRequest); diff --git a/src/config/auth.php b/src/config/auth.php --- a/src/config/auth.php +++ b/src/config/auth.php @@ -140,6 +140,11 @@ 'client_secret' => env('PASSPORT_SYNAPSE_OAUTH_CLIENT_SECRET'), ], + 'sso' => [ + 'client_id' => env('PASSPORT_WEBMAIL_SSO_CLIENT_ID'), + 'client_secret' => env('PASSPORT_WEBMAIL_SSO_CLIENT_SECRET'), + ], + 'token_expiry_minutes' => env('OAUTH_TOKEN_EXPIRY', 60), 'refresh_token_expiry_minutes' => env('OAUTH_REFRESH_TOKEN_EXPIRY', 30 * 24 * 60), ]; diff --git a/src/config/openid.php b/src/config/openid.php --- a/src/config/openid.php +++ b/src/config/openid.php @@ -14,6 +14,8 @@ // 'phone' => 'Information about your phone numbers', // 'address' => 'Information about your address', // 'login' => 'See your login information', + // FIXME: Need a better label, it's not really a one-time pass. + 'otp' => 'Temporary access to the account', ], ], @@ -30,6 +32,9 @@ // 'company_phone', // 'company_email', // ], + 'otp' => [ + 'otp', + ] ], /** diff --git a/src/resources/lang/en/auth.php b/src/resources/lang/en/auth.php --- a/src/resources/lang/en/auth.php +++ b/src/resources/lang/en/auth.php @@ -21,7 +21,7 @@ 'error.password' => "Invalid password", 'error.invalidrequest' => "Invalid authorization request.", 'error.geolocation' => "Country code mismatch", - 'error.nofound' => "User not found", + 'error.notfound' => "User not found", 'error.2fa' => "Second factor failure", 'error.2fa-generic' => "Second factor failure", diff --git a/src/tests/Feature/Controller/AuthTest.php b/src/tests/Feature/Controller/AuthTest.php --- a/src/tests/Feature/Controller/AuthTest.php +++ b/src/tests/Feature/Controller/AuthTest.php @@ -471,15 +471,15 @@ public function testOIDCAuthorizationCodeFlow(): void { $user = $this->getTestUser('john@kolab.org'); - $client = \App\Auth\PassportClient::find(\config('auth.synapse.client_id')); + $client = \App\Auth\PassportClient::find(\config('auth.sso.client_id')); // Note: Invalid input cases were tested above, we omit them here - // This is essentially the same as for OAuth2, but with extended scope + // This is essentially the same as for OAuth2, but with extended scopes $post = [ 'client_id' => $client->id, 'response_type' => 'code', - 'scope' => 'openid email', + 'scope' => 'openid email otp', 'state' => 'state', 'nonce' => 'nonce', ]; @@ -522,8 +522,10 @@ $this->assertSame('JWT', $token['typ']); $this->assertSame('RS256', $token['alg']); + $this->assertSame('nonce', $token['nonce']); $this->assertSame(url('/'), $token['iss']); $this->assertSame($user->email, $token['email']); + $this->assertSame((string) $user->id, \App\Auth\Utils::tokenValidate($token['otp'])); // TODO: Validate JWT token properly diff --git a/src/tests/Feature/Controller/WellKnownTest.php b/src/tests/Feature/Controller/WellKnownTest.php --- a/src/tests/Feature/Controller/WellKnownTest.php +++ b/src/tests/Feature/Controller/WellKnownTest.php @@ -11,7 +11,7 @@ */ public function testOpenidConfiguration(): void { - $href = \App\Utils::serviceUrl("/"); + $href = \App\Utils::serviceUrl('/'); $response = $this->get('.well-known/openid-configuration'); $response->assertStatus(200)