diff --git a/bin/podman_shared b/bin/podman_shared --- a/bin/podman_shared +++ b/bin/podman_shared @@ -293,9 +293,12 @@ podman__run_synapse() { $PODMAN run -dt --pod $POD --name $POD-synapse --replace \ - $SYNAPSE_STORAGE \ + $SYNAPSE_STORAGE \ + -v $CERTS_PATH:/etc/certs:ro \ -e APP_DOMAIN \ -e KOLAB_URL="http://127.0.0.1:8000" \ + -e SYNAPSE_OAUTH_CLIENT_ID="${PASSPORT_SYNAPSE_OAUTH_CLIENT_ID:?"missing env variable"}" \ + -e SYNAPSE_OAUTH_CLIENT_SECRET="${PASSPORT_SYNAPSE_OAUTH_CLIENT_SECRET:?"missing env variable"}" \ synapse:latest } diff --git a/ci/env b/ci/env --- a/ci/env +++ b/ci/env @@ -151,6 +151,8 @@ MEET_SERVER_TOKEN=simple123 PASSPORT_PROXY_OAUTH_CLIENT_ID=5909ca4f-df7e-45fe-b355-e7c195aef117 PASSPORT_PROXY_OAUTH_CLIENT_SECRET=3URb+3JGJM9wPuDnlUSTPOw2mqmHsoOV8NXanx9xwQM= +PASSPORT_SYNAPSE_OAUTH_CLIENT_ID=2909ca4f-df7e-45fe-b355-e7c195aef112 +PASSPORT_SYNAPSE_OAUTH_CLIENT_SECRET=2URb+3JGJM9wPuDnlUSTPOw2mqmHsoOV8NXanx9xwQM= DES_KEY=kBxUM/53N9p9abusAoT0ZEAxwI2pxFz/ KOLAB_GIT_REF=master diff --git a/ci/testctl b/ci/testctl --- a/ci/testctl +++ b/ci/testctl @@ -112,7 +112,8 @@ export MARIADB_STORAGE=--mount=type=tmpfs,tmpfs-size=512M,destination=/var/lib/mysql,U=true export REDIS_STORAGE=--mount=type=tmpfs,tmpfs-size=128M,destination=/var/lib/redis,U=true export MINIO_STORAGE=--mount=type=tmpfs,tmpfs-size=128M,destination=/data,U=true - +export PASSPORT_SYNAPSE_OAUTH_CLIENT_ID=2909ca4f-df7e-45fe-b355-e7c195aef112 +export PASSPORT_SYNAPSE_OAUTH_CLIENT_SECRET=2URb+3JGJM9wPuDnlUSTPOw2mqmHsoOV8NXanx9xwQM= export PODMAN_IGNORE_CGROUPSV1_WARNING=true 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 @@ -30,5 +30,21 @@ ]); $client->id = \config('auth.proxy.client_id'); $client->save(); + + //Create a client for synapse oauth + #FIXME not sure about the provider + $client = Passport::client()->forceFill([ + 'user_id' => null, + 'name' => "Synapse oauth client", + 'secret' => \config('auth.synapse.client_secret'), + 'provider' => 'users', + 'redirect' => 'https://' . \config('app.website_domain') . "/_synapse/client/oidc/callback", + 'personal_access_client' => 0, + 'password_client' => 0, + 'revoked' => false, + 'allowed_scopes' => ['oauth'], + ]); + $client->id = \config('auth.synapse.client_id'); + $client->save(); } } diff --git a/config.prod/src/database/seeds/PassportSeeder.php b/config.prod/src/database/seeds/PassportSeeder.php --- a/config.prod/src/database/seeds/PassportSeeder.php +++ b/config.prod/src/database/seeds/PassportSeeder.php @@ -30,5 +30,20 @@ ]); $client->id = \config('auth.proxy.client_id'); $client->save(); + + //Create a client for synapse oauth + $client = Passport::client()->forceFill([ + 'user_id' => null, + 'name' => "Synapse oauth client", + 'secret' => \config('auth.synapse.client_secret'), + 'provider' => 'users', + 'redirect' => 'https://' . \config('app.website_domain') . "/_synapse/client/oidc/callback", + 'personal_access_client' => 0, + 'password_client' => 0, + 'revoked' => false, + 'allowed_scopes' => ['oauth'], + ]); + $client->id = \config('auth.synapse.client_id'); + $client->save(); } } diff --git a/docker/synapse/Dockerfile b/docker/synapse/Dockerfile --- a/docker/synapse/Dockerfile +++ b/docker/synapse/Dockerfile @@ -20,14 +20,14 @@ openssl-devel \ sed \ wget && \ - pip3 install matrix-synapse && \ + pip3 install matrix-synapse authlib && \ dnf clean all COPY /rootfs / RUN id default || (groupadd -g 1001 default && useradd -d /opt/app-root/ -u 1001 -g 1001 default) -RUN PATHS=(/opt/app-root/src) && \ +RUN PATHS=(/opt/app-root/src /etc/pki/ca-trust/extracted/ /etc/pki/ca-trust/source/anchors/) && \ mkdir -p ${PATHS[@]} && \ chmod -R 777 ${PATHS[@]} && \ chown -R 1001:0 ${PATHS[@]} && \ diff --git a/docker/synapse/rootfs/opt/app-root/src/homeserver.yaml b/docker/synapse/rootfs/opt/app-root/src/homeserver.yaml --- a/docker/synapse/rootfs/opt/app-root/src/homeserver.yaml +++ b/docker/synapse/rootfs/opt/app-root/src/homeserver.yaml @@ -109,22 +109,32 @@ federation_domain_whitelist: - APP_DOMAIN -# oidc_providers: -# - idp_id: kolab -# idp_name: "Kolab" -# issuer: "https://127.0.0.1:8443/auth/realms/{realm_name}" -# client_id: "synapse" -# client_secret: "copy secret generated from above" -# scopes: ["openid", "profile"] -# user_mapping_provider: -# config: -# localpart_template: "\{\{ user.preferred_username }}" -# display_name_template: "\{\{ user.name }}" +sso: + client_whitelist: + - https://APP_DOMAIN/ + update_profile_information: true + +oidc_providers: + - idp_id: kolab + idp_name: "Kolab" + discover: false + issuer: "https://APP_DOMAIN" + authorization_endpoint: "https://APP_DOMAIN/authorize" + #These connections go over localhost, but must still be https (otherwise it doesn't work). Also the certificate must match, so we can't use 127.0.0.1. + token_endpoint: "https://APP_DOMAIN:6443/oauth/token" + userinfo_endpoint: "https://APP_DOMAIN:6443/api/oauth/info" + client_id: "SYNAPSE_OAUTH_CLIENT_ID" + client_secret: "SYNAPSE_OAUTH_CLIENT_SECRET" + client_auth_method: client_secret_post + allow_existing_users: true + allow_registration: false + scopes: ['oauth'] + user_mapping_provider: + config: + subject_claim: "id" + email_template: "{{ user.email }}" + display_name_template: "{{ user.settings.first_name }}" -modules: -- module: kolab_auth_provider.KolabAuthProvider - config: - kolab_url: "KOLAB_URL" ## API Configuration ## diff --git a/docker/synapse/rootfs/opt/app-root/src/init.sh b/docker/synapse/rootfs/opt/app-root/src/init.sh --- a/docker/synapse/rootfs/opt/app-root/src/init.sh +++ b/docker/synapse/rootfs/opt/app-root/src/init.sh @@ -1,11 +1,18 @@ #!/bin/bash set -e +if [[ -f /etc/certs/ca.cert ]]; then + cp /etc/certs/ca.cert /etc/pki/ca-trust/source/anchors/ + update-ca-trust +fi + sed -i -r \ -e "s|APP_DOMAIN|$APP_DOMAIN|g" \ -e "s|KOLAB_URL|$KOLAB_URL|g" \ -e "s|TURN_SHARED_SECRET|$TURN_SHARED_SECRET|g" \ -e "s|TURN_URIS|$TURN_URIS|g" \ + -e "s|SYNAPSE_OAUTH_CLIENT_ID|$SYNAPSE_OAUTH_CLIENT_ID|g" \ + -e "s|SYNAPSE_OAUTH_CLIENT_SECRET|$SYNAPSE_OAUTH_CLIENT_SECRET|g" \ /opt/app-root/src/homeserver.yaml exec synctl --no-daemonize start ${CONFIGFILE:-/opt/app-root/src/homeserver.yaml} diff --git a/kolabctl b/kolabctl --- a/kolabctl +++ b/kolabctl @@ -110,6 +110,16 @@ echo "PASSPORT_PROXY_OAUTH_CLIENT_SECRET=${PASSPORT_PROXY_OAUTH_CLIENT_SECRET}" >> src/.env fi + if ! grep -q "PASSPORT_SYNAPSE_OAUTH_CLIENT_ID=" src/.env; then + PASSPORT_SYNAPSE_OAUTH_CLIENT_ID=$(uuidgen); + echo "PASSPORT_SYNAPSE_OAUTH_CLIENT_ID=${PASSPORT_SYNAPSE_OAUTH_CLIENT_ID}" >> src/.env + fi + + if ! grep -q "PASSPORT_SYNAPSE_OAUTH_CLIENT_SECRET=" src/.env; then + PASSPORT_SYNAPSE_OAUTH_CLIENT_SECRET=$(openssl rand -base64 32); + echo "PASSPORT_SYNAPSE_OAUTH_CLIENT_SECRET=${PASSPORT_SYNAPSE_OAUTH_CLIENT_SECRET}" >> src/.env + fi + if ! grep -q "PASSPORT_PUBLIC_KEY=|PASSPORT_PRIVATE_KEY=" src/.env; then PASSPORT_PRIVATE_KEY=$(openssl genrsa 4096); echo "PASSPORT_PRIVATE_KEY=\"${PASSPORT_PRIVATE_KEY}\"" >> src/.env 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 @@ -9,6 +9,9 @@ use Illuminate\Support\Facades\Validator; use Laravel\Passport\TokenRepository; use Laravel\Passport\RefreshTokenRepository; +use League\OAuth2\Server\AuthorizationServer; +use Psr\Http\Message\ServerRequestInterface; +use Nyholm\Psr7\Response as Psr7Response; class AuthController extends Controller { @@ -87,6 +90,49 @@ return self::logonResponse($user, $request->password, $request->secondfactor); } + /** + * Approval request for the oauth authorization endpoint + * + * + * * The user is authenticated via the regular login page + * * We assume implicit consent in the Authorization page + * * Ultimately we return an authorization code to the caller via the redirect_uri + * + * The implementation is based on Laravel\Passport\Http\Controllers\AuthorizationController + * + * @param \Illuminate\Http\Request $request The API request. + * + * @return \Illuminate\Http\JsonResponse + */ + public function oauthApprove( + ServerRequestInterface $psrRequest, + Request $request, + AuthorizationServer $server + ) { + if ($request->response_type != "code") { + return response()->json(['status' => 'error', 'errors' => ["invalid response_type"]], 422); + } + + $user = Auth::guard()->user(); + if (!$user) { + \Log::warning("User not found"); + return response()->json(['status' => 'error', 'message' => self::trans('auth.failed')], 401); + } + + try { + $authRequest = $server->validateAuthorizationRequest($psrRequest); + } catch (\Exception $e) { + return response()->json(['status' => 'error', 'message' => "Failed to validate"], 401); + } + + //TODO I'm not sure if we should still execute this to deny the request + $authRequest->setUser(new \Laravel\Passport\Bridge\User($user->getAuthIdentifier())); + $authRequest->setAuthorizationApproved(true); + + # This will generate a 302 redirect to the redirect_uri with the generated authorization code + return $server->completeAuthorizationRequest($authRequest, new Psr7Response()); + } + /** * Get the user (geo) location * diff --git a/src/app/Providers/AppServiceProvider.php b/src/app/Providers/AppServiceProvider.php --- a/src/app/Providers/AppServiceProvider.php +++ b/src/app/Providers/AppServiceProvider.php @@ -16,8 +16,6 @@ */ public function register(): void { - Passport::enablePasswordGrant(); - Passport::ignoreRoutes(); } /** diff --git a/src/app/Providers/PassportServiceProvider.php b/src/app/Providers/PassportServiceProvider.php --- a/src/app/Providers/PassportServiceProvider.php +++ b/src/app/Providers/PassportServiceProvider.php @@ -17,10 +17,14 @@ */ public function boot() { + Passport::enablePasswordGrant(); + Passport::ignoreRoutes(); + Passport::tokensCan([ 'api' => 'Access API', 'mfa' => 'Access MFA API', 'fs' => 'Access Files API', + 'oauth' => 'Access OAUTH API', ]); Passport::tokensExpireIn(now()->addMinutes(\config('auth.token_expiry_minutes'))); diff --git a/src/config/auth.php b/src/config/auth.php --- a/src/config/auth.php +++ b/src/config/auth.php @@ -135,6 +135,11 @@ 'client_secret' => env('PASSPORT_PROXY_OAUTH_CLIENT_SECRET'), ], + 'synapse' => [ + 'client_id' => env('PASSPORT_SYNAPSE_OAUTH_CLIENT_ID'), + 'client_secret' => env('PASSPORT_SYNAPSE_OAUTH_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/resources/js/user/routes.js b/src/resources/js/user/routes.js --- a/src/resources/js/user/routes.js +++ b/src/resources/js/user/routes.js @@ -1,4 +1,5 @@ import LoginComponent from '../../vue/Login' +import AuthorizeComponent from '../../vue/Authorize' import LogoutComponent from '../../vue/Logout' import PageComponent from '../../vue/Page' import PasswordResetComponent from '../../vue/PasswordReset' @@ -85,6 +86,12 @@ component: FileListComponent, meta: { requiresAuth: true, perm: 'files' } }, + { + path: '/authorize', + name: 'authorize', + component: AuthorizeComponent, + meta: { requiresAuth: true } + }, { path: '/login', name: 'login', diff --git a/src/resources/vue/Authorize.vue b/src/resources/vue/Authorize.vue new file mode 100644 --- /dev/null +++ b/src/resources/vue/Authorize.vue @@ -0,0 +1,61 @@ + + + diff --git a/src/routes/api.php b/src/routes/api.php --- a/src/routes/api.php +++ b/src/routes/api.php @@ -15,6 +15,27 @@ | */ +Route::group( + [ + 'middleware' => 'api', + 'prefix' => 'oauth' + ], + function () { + Route::group( + ['middleware' => ['auth:api', 'scope:oauth']], + function () { + Route::get('info', [API\AuthController::class, 'info']); + }, + ); + Route::group( + ['middleware' => 'auth:api'], + function () { + Route::post('approve', [API\AuthController::class, 'oauthApprove']); + } + ); + } +); + Route::group( [ 'middleware' => 'api',