diff --git a/docker/synapse/rootfs/opt/app-root/src/homeserver.yaml b/docker/synapse/rootfs/opt/app-root/src/homeserver.yaml index 7ae200d5..8e030f5c 100644 --- a/docker/synapse/rootfs/opt/app-root/src/homeserver.yaml +++ b/docker/synapse/rootfs/opt/app-root/src/homeserver.yaml @@ -1,157 +1,157 @@ server_name: "APP_DOMAIN" public_baseurl: "https://APP_DOMAIN" pid_file: /opt/app-root/src/homeserver.pid listeners: - port: 8008 tls: false type: http x_forwarded: true bind_addresses: ['::'] resources: - names: [client, federation] compress: false database: name: sqlite3 args: database: /data/homeserver.db log_config: "/opt/app-root/src/log.config" web_client: False soft_file_limit: 0 # We have no registration # registration_shared_secret: "REGISTRATION_SHARED_SECRET" # We just use a default derived from the signing key # macaroon_secret_key: "MACAROON_SECRET_KEY" # Only required for consent forms that we don't use # form_secret: "FORM_SECRET" report_stats: false enable_metrics: false signing_key_path: "/data/signing.key" old_signing_keys: {} key_refresh_interval: "1d" trusted_key_servers: [] ## Performance ## event_cache_size: "10K" ## Ratelimiting ## rc_messages_per_second: 0.2 rc_message_burst_count: 10.0 federation_rc_window_size: 1000 federation_rc_sleep_limit: 10 federation_rc_sleep_delay: 500 federation_rc_reject_limit: 50 federation_rc_concurrent: 3 ## Files ## media_store_path: /data/media_store max_upload_size: 50M max_image_pixels: 32M dynamic_thumbnails: false # media_retention: # local_media_lifetime: 90d # remote_media_lifetime: 14d # List of thumbnail to precalculate when an image is uploaded. thumbnail_sizes: - width: 32 height: 32 method: crop - width: 96 height: 96 method: crop - width: 320 height: 240 method: scale - width: 640 height: 480 method: scale - width: 800 height: 600 method: scale url_preview_enabled: False max_spider_size: "10M" ## Captcha ## enable_registration_captcha: False ## Turn ## turn_uris: [TURN_URIS] turn_shared_secret: "TURN_SHARED_SECRET" turn_user_lifetime: "1h" turn_allow_guests: false ## Registration ## enable_registration: false enable_registration_without_verification: false bcrypt_rounds: 12 allow_guest_access: false enable_group_creation: false inhibit_user_in_use_error: true user_directory: enabled: false search_all_users: false prefer_local_users: false allow_public_rooms_without_auth: false enable_set_displayname: false enable_set_avatar_url: false enable_3pid_changes: false # Avoid leaking profile information require_auth_for_profile_requests: true limit_profile_requests_to_users_who_share_rooms: true include_profile_data_on_invite: false federation_domain_whitelist: - APP_DOMAIN 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" + authorization_endpoint: "https://APP_DOMAIN/oauth/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" + userinfo_endpoint: "https://APP_DOMAIN:6443/oauth/userinfo" 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'] + scopes: ['email'] user_mapping_provider: config: subject_claim: "id" email_template: "{{ user.email }}" - display_name_template: "{{ user.settings.first_name }}" + display_name_template: "{{ user.settings.name }}" ## API Configuration ## # app_service_config_files: # - /config/hookshot.yaml expire_access_token: false password_config: enabled: true localdb_enabled: false # Configure a default retention policy (can be overriden ber room) retention: allowed_lifetime_min: 1d allowed_lifetime_max: 1y default_policy: min_lifetime: 1d max_lifetime: 1y diff --git a/src/app/Http/Controllers/API/AuthController.php b/src/app/Http/Controllers/API/AuthController.php index 9302c6d0..db17d128 100644 --- a/src/app/Http/Controllers/API/AuthController.php +++ b/src/app/Http/Controllers/API/AuthController.php @@ -1,277 +1,279 @@ user(); if (!empty(request()->input('refresh'))) { return $this->refreshAndRespond(request(), $user); } $response = V4\UsersController::userResponse($user); return response()->json($response); } /** * Helper method for other controllers with user auto-logon * functionality * * @param \App\User $user User model object * @param string $password Plain text password * @param string|null $secondFactor Second factor code if available */ public static function logonResponse(User $user, string $password, string $secondFactor = null) { $proxyRequest = Request::create('/oauth/token', 'POST', [ 'username' => $user->email, 'password' => $password, 'grant_type' => 'password', 'client_id' => \config('auth.proxy.client_id'), 'client_secret' => \config('auth.proxy.client_secret'), 'scope' => 'api', 'secondfactor' => $secondFactor ]); $proxyRequest->headers->set('X-Client-IP', request()->ip()); $tokenResponse = app()->handle($proxyRequest); return self::respondWithToken($tokenResponse, $user); } /** * Get an oauth token via given credentials. * * @param \Illuminate\Http\Request $request The API request. * * @return \Illuminate\Http\JsonResponse */ public function login(Request $request) { $v = Validator::make( $request->all(), [ 'email' => 'required|min:3', 'password' => 'required|min:1', ] ); if ($v->fails()) { return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); } $user = \App\User::where('email', $request->email)->first(); if (!$user) { return response()->json(['status' => 'error', 'message' => self::trans('auth.failed')], 401); } 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 ServerRequestInterface $psrRequest PSR request * @param \Illuminate\Http\Request $request The API request * @param AuthorizationServer $server Authorization server * * @return \Illuminate\Http\JsonResponse */ public function oauthApprove(ServerRequestInterface $psrRequest, Request $request, AuthorizationServer $server) { if ($request->response_type != 'code') { return self::errorResponse(422, self::trans('validation.invalidvalueof', ['attribute' => 'response_type'])); } try { // league/oauth2-server/src/Grant/ code expects GET parameters, but we're using POST here $psrRequest = $psrRequest->withQueryParams($request->input()); $authRequest = $server->validateAuthorizationRequest($psrRequest); $user = Auth::guard()->user(); // 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 $response = $server->completeAuthorizationRequest($authRequest, new Psr7Response()); } catch (\League\OAuth2\Server\Exception\OAuthServerException $e) { // Note: We don't want 401 or 400 codes here, use 422 which is used in our API $code = $e->getHttpStatusCode(); return self::errorResponse($code < 500 ? 422 : 500, $e->getMessage()); } catch (\Exception $e) { return self::errorResponse(422, self::trans('auth.error.invalidrequest')); } return response()->json([ 'status' => 'success', 'redirectUrl' => $response->getHeader('Location')[0], ]); } /** * Get the authenticated User information (using access token claims) * * @return \Illuminate\Http\JsonResponse */ public function oauthUserInfo() { $user = Auth::guard()->user(); $response = [ // Per OIDC spec. 'sub' must be always returned 'sub' => $user->id, ]; if ($user->tokenCan('email')) { $response['email'] = $user->email; $response['email_verified'] = $user->isActive(); + # At least synapse depends on a "settings" structure being available + $response['settings'] = [ 'name' => $user->name() ]; } // TODO: Other claims (https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims) // address: address // phone: phone_number and phone_number_verified // profile: name, family_name, given_name, middle_name, nickname, preferred_username, // profile, picture, website, gender, birthdate, zoneinfo, locale, and updated_at return response()->json($response); } /** * Get the user (geo) location * * @return \Illuminate\Http\JsonResponse */ public function location() { $ip = request()->ip(); $response = [ 'ipAddress' => $ip, 'countryCode' => \App\Utils::countryForIP($ip, ''), ]; return response()->json($response); } /** * Log the user out (Invalidate the token) * * @return \Illuminate\Http\JsonResponse */ public function logout() { $tokenId = Auth::user()->token()->id; $tokenRepository = app(TokenRepository::class); $refreshTokenRepository = app(RefreshTokenRepository::class); // Revoke an access token... $tokenRepository->revokeAccessToken($tokenId); // Revoke all of the token's refresh tokens... $refreshTokenRepository->revokeRefreshTokensByAccessTokenId($tokenId); return response()->json([ 'status' => 'success', 'message' => self::trans('auth.logoutsuccess') ]); } /** * Refresh a token. * * @return \Illuminate\Http\JsonResponse */ public function refresh(Request $request) { return self::refreshAndRespond($request); } /** * Refresh the token and respond with it. * * @param \Illuminate\Http\Request $request The API request. * @param ?\App\User $user The user being authenticated * * @return \Illuminate\Http\JsonResponse */ protected static function refreshAndRespond(Request $request, $user = null) { $proxyRequest = Request::create('/oauth/token', 'POST', [ 'grant_type' => 'refresh_token', 'refresh_token' => $request->refresh_token, 'client_id' => \config('auth.proxy.client_id'), 'client_secret' => \config('auth.proxy.client_secret'), ]); $tokenResponse = app()->handle($proxyRequest); return self::respondWithToken($tokenResponse, $user); } /** * Get the token array structure. * * @param \Symfony\Component\HttpFoundation\Response $tokenResponse The response containing the token. * @param ?\App\User $user The user being authenticated * * @return \Illuminate\Http\JsonResponse */ protected static function respondWithToken($tokenResponse, $user = null) { $data = json_decode($tokenResponse->getContent()); if ($tokenResponse->getStatusCode() != 200) { if (isset($data->error) && $data->error == 'secondfactor' && isset($data->error_description)) { $errors = ['secondfactor' => $data->error_description]; return response()->json(['status' => 'error', 'errors' => $errors], 422); } return response()->json(['status' => 'error', 'message' => self::trans('auth.failed')], 401); } if ($user) { $response = V4\UsersController::userResponse($user); } else { $response = []; } $response['status'] = 'success'; $response['access_token'] = $data->access_token; $response['refresh_token'] = $data->refresh_token; $response['token_type'] = 'bearer'; $response['expires_in'] = $data->expires_in; return response()->json($response); } }