silverstripe /
silverstripe-framework
| 1 | <?php |
||
| 2 | |||
| 3 | namespace SilverStripe\Security; |
||
| 4 | |||
| 5 | use SilverStripe\Control\Controller; |
||
| 6 | use SilverStripe\Control\Director; |
||
| 7 | use SilverStripe\Control\HTTPRequest; |
||
| 8 | use SilverStripe\Control\HTTPResponse; |
||
| 9 | use SilverStripe\Control\HTTPResponse_Exception; |
||
| 10 | use SilverStripe\Core\Config\Configurable; |
||
| 11 | use SilverStripe\Core\Environment; |
||
| 12 | use SilverStripe\Core\Injector\Injector; |
||
| 13 | use SilverStripe\ORM\Connect\DatabaseException; |
||
| 14 | use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator; |
||
| 15 | |||
| 16 | /** |
||
| 17 | * Provides an interface to HTTP basic authentication. |
||
| 18 | * |
||
| 19 | * This utility class can be used to secure any request processed by SilverStripe with basic authentication. |
||
| 20 | * To do so, {@link BasicAuth::requireLogin()} from your Controller's init() method or action handler method. |
||
| 21 | * |
||
| 22 | * It also has a function to protect your entire site. See {@link BasicAuth::protect_entire_site()} |
||
| 23 | * for more information. You can control this setting on controller-level by using {@link Controller->basicAuthEnabled}. |
||
| 24 | * |
||
| 25 | * CAUTION: Basic Auth is an oudated security measure which passes credentials without encryption over the network. |
||
| 26 | * It is considered insecure unless this connection itself is secured (via HTTPS). |
||
| 27 | * It also doesn't prevent access to web requests which aren't handled via SilverStripe (e.g. published assets). |
||
| 28 | * Consider using additional authentication and authorisation measures to secure access (e.g. IP whitelists). |
||
| 29 | */ |
||
| 30 | class BasicAuth |
||
| 31 | { |
||
| 32 | use Configurable; |
||
| 33 | |||
| 34 | /** |
||
| 35 | * Env var to set to enable basic auth |
||
| 36 | */ |
||
| 37 | const USE_BASIC_AUTH = 'SS_USE_BASIC_AUTH'; |
||
| 38 | |||
| 39 | /** |
||
| 40 | * Default permission code |
||
| 41 | */ |
||
| 42 | const AUTH_PERMISSION = 'ADMIN'; |
||
| 43 | |||
| 44 | /** |
||
| 45 | * @config |
||
| 46 | * @var Boolean Flag set by {@link self::protect_entire_site()} |
||
| 47 | */ |
||
| 48 | private static $entire_site_protected = false; |
||
|
0 ignored issues
–
show
introduced
by
Loading history...
|
|||
| 49 | |||
| 50 | /** |
||
| 51 | * Set to true to ignore in CLI mode |
||
| 52 | * |
||
| 53 | * @var bool |
||
| 54 | */ |
||
| 55 | private static $ignore_cli = true; |
||
|
0 ignored issues
–
show
|
|||
| 56 | |||
| 57 | /** |
||
| 58 | * @config |
||
| 59 | * @var String|array Holds a {@link Permission} code that is required |
||
| 60 | * when calling {@link protect_site_if_necessary()}. Set this value through |
||
| 61 | * {@link protect_entire_site()}. |
||
| 62 | */ |
||
| 63 | private static $entire_site_protected_code = 'ADMIN'; |
||
|
0 ignored issues
–
show
|
|||
| 64 | |||
| 65 | /** |
||
| 66 | * @config |
||
| 67 | * @var String Message that shows in the authentication box. |
||
| 68 | * Set this value through {@link protect_entire_site()}. |
||
| 69 | */ |
||
| 70 | private static $entire_site_protected_message = 'SilverStripe test website. Use your CMS login.'; |
||
|
0 ignored issues
–
show
|
|||
| 71 | |||
| 72 | /** |
||
| 73 | * Require basic authentication. Will request a username and password if none is given. |
||
| 74 | * |
||
| 75 | * Used by {@link Controller::init()}. |
||
| 76 | * |
||
| 77 | * @param HTTPRequest $request |
||
| 78 | * @param string $realm |
||
| 79 | * @param string|array $permissionCode Optional |
||
| 80 | * @param boolean $tryUsingSessionLogin If true, then the method with authenticate against the |
||
| 81 | * session log-in if those credentials are disabled. |
||
| 82 | * @return bool|Member |
||
| 83 | * @throws HTTPResponse_Exception |
||
| 84 | */ |
||
| 85 | public static function requireLogin( |
||
| 86 | HTTPRequest $request, |
||
| 87 | $realm, |
||
| 88 | $permissionCode = null, |
||
| 89 | $tryUsingSessionLogin = true |
||
| 90 | ) { |
||
| 91 | if ((Director::is_cli() && static::config()->get('ignore_cli'))) { |
||
| 92 | return true; |
||
| 93 | } |
||
| 94 | |||
| 95 | $member = null; |
||
| 96 | |||
| 97 | try { |
||
| 98 | if ($request->getHeader('PHP_AUTH_USER') && $request->getHeader('PHP_AUTH_PW')) { |
||
| 99 | /** @var MemberAuthenticator $authenticator */ |
||
| 100 | $authenticators = Security::singleton()->getApplicableAuthenticators(Authenticator::LOGIN); |
||
| 101 | |||
| 102 | foreach ($authenticators as $name => $authenticator) { |
||
| 103 | $member = $authenticator->authenticate([ |
||
| 104 | 'Email' => $request->getHeader('PHP_AUTH_USER'), |
||
| 105 | 'Password' => $request->getHeader('PHP_AUTH_PW'), |
||
| 106 | ], $request); |
||
| 107 | if ($member instanceof Member) { |
||
| 108 | break; |
||
| 109 | } |
||
| 110 | } |
||
| 111 | } |
||
| 112 | } catch (DatabaseException $e) { |
||
| 113 | // Database isn't ready, let people in |
||
| 114 | return true; |
||
| 115 | } |
||
| 116 | |||
| 117 | if (!$member && $tryUsingSessionLogin) { |
||
| 118 | $member = Security::getCurrentUser(); |
||
| 119 | } |
||
| 120 | |||
| 121 | // If we've failed the authentication mechanism, then show the login form |
||
| 122 | if (!$member) { |
||
| 123 | $response = new HTTPResponse(null, 401); |
||
| 124 | $response->addHeader('WWW-Authenticate', "Basic realm=\"$realm\""); |
||
| 125 | |||
| 126 | if ($request->getHeader('PHP_AUTH_USER')) { |
||
| 127 | $response->setBody( |
||
| 128 | _t( |
||
| 129 | 'SilverStripe\\Security\\BasicAuth.ERRORNOTREC', |
||
| 130 | "That username / password isn't recognised" |
||
| 131 | ) |
||
| 132 | ); |
||
| 133 | } else { |
||
| 134 | $response->setBody( |
||
| 135 | _t( |
||
| 136 | 'SilverStripe\\Security\\BasicAuth.ENTERINFO', |
||
| 137 | 'Please enter a username and password.' |
||
| 138 | ) |
||
| 139 | ); |
||
| 140 | } |
||
| 141 | |||
| 142 | // Exception is caught by RequestHandler->handleRequest() and will halt further execution |
||
| 143 | $e = new HTTPResponse_Exception(null, 401); |
||
| 144 | $e->setResponse($response); |
||
| 145 | throw $e; |
||
| 146 | } |
||
| 147 | |||
| 148 | if ($permissionCode && !Permission::checkMember($member->ID, $permissionCode)) { |
||
| 149 | $response = new HTTPResponse(null, 401); |
||
| 150 | $response->addHeader('WWW-Authenticate', "Basic realm=\"$realm\""); |
||
| 151 | |||
| 152 | if ($request->getHeader('PHP_AUTH_USER')) { |
||
| 153 | $response->setBody( |
||
| 154 | _t( |
||
| 155 | 'SilverStripe\\Security\\BasicAuth.ERRORNOTADMIN', |
||
| 156 | 'That user is not an administrator.' |
||
| 157 | ) |
||
| 158 | ); |
||
| 159 | } |
||
| 160 | |||
| 161 | // Exception is caught by RequestHandler->handleRequest() and will halt further execution |
||
| 162 | $e = new HTTPResponse_Exception(null, 401); |
||
| 163 | $e->setResponse($response); |
||
| 164 | throw $e; |
||
| 165 | } |
||
| 166 | |||
| 167 | return $member; |
||
| 168 | } |
||
| 169 | |||
| 170 | /** |
||
| 171 | * Enable protection of all requests handed by SilverStripe with basic authentication. |
||
| 172 | * |
||
| 173 | * This log-in uses the Member database for authentication, but doesn't interfere with the |
||
| 174 | * regular log-in form. This can be useful for test sites, where you want to hide the site |
||
| 175 | * away from prying eyes, but still be able to test the regular log-in features of the site. |
||
| 176 | * |
||
| 177 | * You can also enable this feature by adding this line to your .env. Set this to a permission |
||
| 178 | * code you wish to require: `SS_USE_BASIC_AUTH=ADMIN` |
||
| 179 | * |
||
| 180 | * CAUTION: Basic Auth is an oudated security measure which passes credentials without encryption over the network. |
||
| 181 | * It is considered insecure unless this connection itself is secured (via HTTPS). |
||
| 182 | * It also doesn't prevent access to web requests which aren't handled via SilverStripe (e.g. published assets). |
||
| 183 | * Consider using additional authentication and authorisation measures to secure access (e.g. IP whitelists). |
||
| 184 | * |
||
| 185 | * @param boolean $protect Set this to false to disable protection. |
||
| 186 | * @param string $code {@link Permission} code that is required from the user. |
||
| 187 | * Defaults to "ADMIN". Set to NULL to just require a valid login, regardless |
||
| 188 | * of the permission codes a user has. |
||
| 189 | * @param string $message |
||
| 190 | */ |
||
| 191 | public static function protect_entire_site($protect = true, $code = self::AUTH_PERMISSION, $message = null) |
||
| 192 | { |
||
| 193 | static::config() |
||
| 194 | ->set('entire_site_protected', $protect) |
||
| 195 | ->set('entire_site_protected_code', $code); |
||
| 196 | if ($message) { |
||
| 197 | static::config()->set('entire_site_protected_message', $message); |
||
| 198 | } |
||
| 199 | } |
||
| 200 | |||
| 201 | /** |
||
| 202 | * Call {@link BasicAuth::requireLogin()} if {@link BasicAuth::protect_entire_site()} has been called. |
||
| 203 | * This is a helper function used by {@link Controller::init()}. |
||
| 204 | * |
||
| 205 | * If you want to enabled protection (rather than enforcing it), |
||
| 206 | * please use {@link protect_entire_site()}. |
||
| 207 | * |
||
| 208 | * @param HTTPRequest|null $request |
||
| 209 | * @throws HTTPResponse_Exception |
||
| 210 | */ |
||
| 211 | public static function protect_site_if_necessary(HTTPRequest $request = null) |
||
| 212 | { |
||
| 213 | $config = static::config(); |
||
| 214 | |||
| 215 | // Check if site is protected |
||
| 216 | if ($config->get('entire_site_protected')) { |
||
| 217 | $permissionCode = $config->get('entire_site_protected_code'); |
||
| 218 | } elseif (Environment::getEnv(self::USE_BASIC_AUTH)) { |
||
| 219 | // Convert legacy 1 / true to ADMIN permissions |
||
| 220 | $permissionCode = Environment::getEnv(self::USE_BASIC_AUTH); |
||
| 221 | if (!is_string($permissionCode) || is_numeric($permissionCode)) { |
||
| 222 | $permissionCode = self::AUTH_PERMISSION; |
||
| 223 | } |
||
| 224 | } else { |
||
| 225 | // Not enabled |
||
| 226 | return; |
||
| 227 | } |
||
| 228 | |||
| 229 | // Get request |
||
| 230 | if (!$request && Injector::inst()->has(HTTPRequest::class)) { |
||
| 231 | $request = Injector::inst()->get(HTTPRequest::class); |
||
| 232 | } |
||
| 233 | |||
| 234 | // Require login |
||
| 235 | static::requireLogin( |
||
| 236 | $request, |
||
| 237 | $config->get('entire_site_protected_message'), |
||
| 238 | $permissionCode, |
||
| 239 | false |
||
| 240 | ); |
||
| 241 | } |
||
| 242 | } |
||
| 243 |