grantholle / laravel-powerschool-auth
使用PowerSchool验证您的Laravel应用程序
Requires
- php: ^8.1
- ext-curl: *
- ext-json: *
- ext-libxml: *
- grantholle/pear-openid: ^2.0
- illuminate/http: ^8.0|^9.0|^10.0|^11.0
- illuminate/support: ^8.0|^9.0|^10.0|^11.0
- spatie/url: ^2.0
Requires (Dev)
- orchestra/testbench: ^7.0|^8.0|^9.0
- spatie/ray: ^1.33
README
将PowerSchool用作您的Laravel应用程序的身份提供商,支持原始OpenID 2.0实现,以及PowerSchool 20.11中引入的OpenID Connect。
OpenID 2.0是PowerSchool内部发送到您应用程序以执行数据交换的链接。它只能从PowerSchool发送到您的应用程序。OpenID Connect是用户从您的应用程序直接对PowerSchool进行认证的方式,并提供更好的用户体验。
安装
composer require grantholle/laravel-powerschool-auth
配置
首先发布配置文件,config/powerschool-auth.php。
php artisan vendor:publish --provider="GrantHolle\PowerSchool\Auth\PowerSchoolAuthServiceProvider"
配置根据不同的用户类型进行分离,staff(员工)、guardian(监护人)和student(学生)。OpenID Connect OAuth流程支持四种类型(staff、teacher、parent、student),但为了灵活性,staff和teacher将合并为单个staff类型。parent OIDC persona将引用配置中的guardian键。
return [ // These are required for OpenID Connect 'server_address' => env('POWERSCHOOL_ADDRESS'), 'client_id' => env('POWERSCHOOL_CLIENT_ID'), 'client_secret' => env('POWERSCHOOL_CLIENT_SECRET'), // User type configuration 'staff' => [ // Setting to false will prevent this user type from authenticating 'allowed' => true, // This is the model to use for a given type // Theoretically you could have different models // for the different user types 'model' => \App\User::class, // These attributes will be synced to your model // PS attribute => your app attribute // Put either OpenID implementation in this // The app will parse whether the key exists in // the response. 'attributes' => [ // These attributes are from OpenID 2.0 'firstName' => 'first_name', 'lastName' => 'last_name', // Shared with 2.0 and Connect 'email' => 'email', // These are OpenID Connect attributes 'given_name' => 'first_name', 'family_name' => 'last_name', ], // The guard used to authenticate your user 'guard' => 'web', // These is the properties used to look up a user's record // OpenID identifier so they can be identified // You will need to have this column already added to // the given model's migration/schema. 'identifying_attributes' => [ 'openid_claimed_id' => 'openid_identity', ], // The path to be redirected to once they are authenticated 'redirectTo' => '', ], // 'guardian' => [ ... // 'student' => [ ... ];
使用方法
这假设您已在PowerSchool实例上安装了一个插件,并在您的plugin.xml文件中有类似以下内容。以下
<?xml version="1.0" encoding="UTF-8"?> <plugin xmlns="http://plugin.powerschool.pearson.com" name="My Plugin" version="1.0" description="An example OpenID plugin."> <oauth base-url="https://example.com" redirect-uri="/auth/powerschool/oidc/verify" allow-client-credential="false" allow-auth-code="true" > <allow-persona>staff</allow-persona> <allow-persona>teacher</allow-persona> </oauth> <openid host="example.com" port="443"> <links> <link display-text="Click here to log in" path="/auth/powerschool/openid" title="An app that uses SSO."> <ui_contexts> <ui_context id="admin.header"/> <ui_context id="admin.left_nav"/> <ui_context id="teacher.header"/> <ui_context id="teacher.pro.apps"/> </ui_contexts> </link> </links> </openid> <publisher name="Example Publisher"> <contact email="publisher@example.com" /> </publisher> </plugin>
有关标签和属性意义的详细信息,请参阅PowerSchool文档。简而言之,oauth标签支持关于OpenID Connect OAuth流程的配置,而openid标签包含关于OpenID 2.0认证的详细信息。安装该插件将在应用程序弹出菜单中注入仅针对员工的链接。现在我们需要创建一个处理与PowerSchool进行认证的控制器。
OpenID 2.0
首先,让我们创建OpenID 2.0认证控制器
php artisan make:controller Auth/PowerSchoolOpenIdLoginController
在控制器生成后,我们需要添加处理所有认证模板代码的特性。
namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use GrantHolle\PowerSchool\Auth\Traits\AuthenticatesUsingPowerSchoolWithOpenId; class PowerSchoolOpenIdLoginController extends Controller { use AuthenticatesUsingPowerSchoolWithOpenId; }
现在让我们将适用的路由添加到您的web.php文件中
// These paths can be whatever you want; the key thing is that they path for `authenticate` // matches what you've configured in your plugin.xml file for the `path` attribute Route::get('/auth/powerschool/openid', [\App\Http\Controllers\Auth\PowerSchoolOpenIdLoginController::class, 'authenticate']); Route::get('/auth/powerschool/openid/verify', [\App\Http\Controllers\Auth\PowerSchoolOpenIdLoginController::class, 'login']) ->name('openid.verify');
默认情况下,验证路由返回PowerSchool的预期为/auth/powerschool/openid/verify,但可以通过覆盖getVerifyRoute()进行更改,如下所述。
一旦用户在PowerSchool中打开您的SSO链接,就会进行OpenID交换,并将数据从PowerSchool提供给您的应用程序。有几个“钩子”可以更改行为,而无需修改底层行为。以下是您可以在控制器中覆盖的函数及其默认行为片段。
namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use GrantHolle\PowerSchool\Auth\Traits\AuthenticatesUsingPowerSchoolWithOpenId; class PowerSchoolOpenIdLoginController extends Controller { use AuthenticatesUsingPowerSchoolWithOpenId; /** * This will get the route to the `login` function after * the authentication request has been sent to PowerSchool * * @return string */ protected function getVerifyRoute(): string { return url('/auth/powerschool/openid/verify'); } /** * This will get the route that should be used after successful authentication. * The user type (staff/guardian/student) is sent as the parameter. * * @param string $userType * @return string */ protected function getRedirectToRoute(string $userType): string { $config = config("powerschool-auth.{$userType}"); return isset($config['redirectTo']) && !empty($config['redirectTo']) ? $config['redirectTo'] : '/home'; } /** * If a user type has `'allowed' => false` in the config, * this is the response to send for that user's attempt. * * @return \Illuminate\Http\Response */ protected function sendNotAllowedResponse() { return response('Forbidden', 403); } /** * Gets the default attributes to be filled for the user * that wouldn't be included in the data exchange with PowerSchool * or that need some custom logic that can't be configured. * The attributes set in the config's `attributes` key will overwrite * these if they are the same. `$data` in this context is the data * received from PowerSchool. For example, you may want to store * the dcid of the user being authenticated. * * @param \Illuminate\Http\Response $request * @param \Illuminate\Support\Collection $data * @return array */ protected function getDefaultAttributes($request, $data): array { return []; } /** * The user has been authenticated. * You can return a custom response here, perform custom actions, etc. * Otherwise, you can change the route in `getRedirectToRoute()`. * * @param \Illuminate\Http\Request $request * @param mixed $user * @param \Illuminate\Support\Collection $data * @return mixed */ protected function authenticated($request, $user, $data) { // } }
OpenID Connect
注意:这需要PowerSchool版本20.11或更高。
OpenID Connect(OIDC)提供了从您的应用程序到使用PowerSchool作为身份提供者的更好的体验。您可以将用户从您的应用程序发送到他们的PowerSchool服务器进行认证,然后带一些信息返回到您的应用程序。为了OIDC能够工作,您必须在.env中包含以下密钥
POWERSCHOOL_ADDRESS=
POWERSCHOOL_CLIENT_ID=
POWERSCHOOL_CLIENT_SECRET=
您会注意到这些与grantholle/powerschool-api共享,以避免重复。
接下来,我们需要创建另一个控制器
php artisan make:controller Auth/PowerSchoolOidcLoginController
在控制器生成后,我们需要添加处理所有认证模板代码的特性。
namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use GrantHolle\PowerSchool\Auth\Traits\AuthenticatesUsingPowerSchoolWithOidc; class PowerSchoolOidcLoginController extends Controller { use AuthenticatesUsingPowerSchoolWithOidc; }
现在让我们将适用的路由添加到您的web.php文件中
// These paths can be whatever you want; the key thing is that they path for the `login` route action to // match what you've configured in your plugin.xml under `oauth`'s `redirect-uri` attribute file for the `path` attribute Route::get('/auth/powerschool/oidc', [\App\Http\Controllers\Auth\PowerSchoolOidcLoginController::class, 'authenticate']); Route::get('/auth/powerschool/oidc/verify', [\App\Http\Controllers\Auth\PowerSchoolOidcLoginController::class, 'login']); // <oauth // base-url="https://example.com" // redirect-uri="/auth/powerschool/oidc/verify" <-- Has to match the route for the `login` action
现在当用户访问您的应用程序中的 /auth/powerschool/oidc 时,OAuth 流程将开始与 PowerSchool。如果用户已经登录到 PowerSchool,它将无缝登录而不会中断。如果他们尚未登录到 PowerSchool,则会根据您在插件中的配置,将他们带到 PowerSchool 的登录提示,通过管理员、教师、家长或学生登录。务必阅读有关 allow-persona 子元素的文档,了解限制可进行身份验证的用户类型的说明。您还可以传递一个 persona 或 _persona 查询变量来告诉 PowerSchool 正在验证的用户类型。这将允许 PowerSchool 跳过用户类型提示并直接进入所需的登录页面。例如
<a href="/auth/powerschool/oidc?persona=parent">Parent sign in</a> <!-- <a href="/auth/powerschool/oidc?persona=teacher">Teacher sign in</a> -->
上述链接将告诉 PowerSchool 正在验证的是家长,因此可以直接将其带到 /public 登录页面。您还可以通过添加一个具有真值(如 1 或 true)的 remember 查询变量来允许更长的认证会话。默认情况下,它是 false。例如
<a href="/auth/powerschool/oidc?remember=1">Sign in with PowerSchool</a>
与 OpenID 2.0 的 "hook" 功能一样,OIDC 特性具有修改用户属性和其他行为的能力。
namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use GrantHolle\PowerSchool\Auth\Traits\AuthenticatesUsingPowerSchoolWithOidc; use Illuminate\Http\Request; use Illuminate\Support\Collection; class PowerSchoolOidcController extends Controller { use AuthenticatesUsingPowerSchoolWithOidc; protected function getRedirectUrl() { return url('/auth/powerschool/oidc'); } /** * Whether to have an extended authenticated session * * @return bool */ protected function remember(): bool { return false; } /** * The scope that this will request from PowerSchool. * By default it requests all scopes for the user. * * @param array $configuration * @return array */ protected function getScope(array $configuration): array { return $configuration['scopes_supported']; } /** * If a user type has `'allowed' => false` in the config, * this is the response to send for that user's attempt. * * @return \Illuminate\Http\Response */ protected function sendNotAllowedResponse() { return response('Forbidden', 403); } /** * Gets the default attributes to be added for this user * * @param Request $request * @param Collection $data * @return array */ protected function getDefaultAttributes(Request $request, Collection $data): array { return []; } /** * The user has been authenticated. * * @param \Illuminate\Http\Request $request * @param mixed $user * @param \Illuminate\Support\Collection $data * @return mixed */ protected function authenticated(Request $request, $user, Collection $data) { // } }
注意事项
目前没有 SAML 集成,因为包含它相当复杂。还有一个问题,为什么要添加它,如果 OpenID 可以工作呢?它更可配置,但也增加了更多的复杂性。一旦我有时间,我会乐意添加它,并且肯定会考虑包含它的拉取请求。
话虽如此,PowerSchool 不支持 <identityAttribute/> 配置来自定义用户的身份属性。对于 OpenID 2.0,据我所知,它默认为 {url}/oid/{usertype}/{username}。在我们的公司,我们经历了如果用户名包含奇怪字符时不需要的行为。例如,在 PowerSchool 中,拥有中/韩文用户名是有效的。发送的标识符只是编码空格,即 {url}/oid/guardian/%20%20%20。幸运的是,电子邮件地址工作得很好。
这也意味着,如果用户的用户名发生变化,并且他们已经在您的应用程序中进行了身份验证,他们将作为新用户进行身份验证,因为他们的 OpenID 标识符也已更改。因此,您可能希望配置不同的属性,例如 email,用作标识属性。这取决于您预期电子邮件或用户名更改的频率。
对于 OpenID Connect,有一个内置的 sub 属性。根据文档
它是已验证用户的唯一且不变的标识符,永远不会为任何未来的用户重用。
这听起来像它将始终是唯一的,尽管有用户名,这是 OpenID 2.0 的问题。然而,如果有共享用户账户(员工有家长账户),这些账户将是分开的。这就是为什么我通常使用电子邮件,尽管存在潜在风险。OpenID Connect 还排除了学生 ID 用于家长和工作人员的行政学校,这是不幸的。您将必须根据用户提供自己的 PowerQuery 来获取该信息。
如果您确实使用电子邮件,我建议在 attribute_transformers 中保留 email 的条目,它返回小写的电子邮件地址。这样,它无关紧要,格式如何到达我们的应用程序,因为它将在我们的数据库中规范化。
attribute_transformers 类只需要有一个接受原始值作为参数的 __invoke() 魔法方法。
[
'staff' => [
'allowed' => true,
'model' => \App\User::class,
'attributes' => [
// PowerSchool attribute => our attribute
'firstName' => 'first_name',
'lastName' => 'last_name',
'email' => 'email',
],
'guard' => 'web',
'identifying_attributes' => [
// PowerSchool attribute => our attribute
'email' => 'email',
],
'attribute_transformers' => [
// PowerSchool attribute => our class
'email' => \GrantHolle\PowerSchool\Auth\Transformers\Lowercase::class,
// See example below
'lastName' => MyTransformer::class,
],
'redirectTo' => '',
],
];
class MyTransformer { public function __invoke($value) { // Manipulate the value somehow return 'Mr./Ms. ' . $value; } }
示例数据
以下是 PowerSchool 的属性交换示例
OpenID 2.0
$data = [ "openid_claimed_id" => "https://my.powerschool.com/oid/admin/jerry.smith", "dcid" => "1234", "usertype" => "staff", "ref" => "https://my.powerschool.com/ws/v1/staff/1234", "email" => "jerry.smith@example.com", "firstName" => "Jerry", "lastName" => "Smith", "districtName" => "My District Name", "districtCustomerNumber" => "AB1234", "districtCountry" => "US", "schoolID" => "1", "usersDCID" => "1234", "teacherNumber" => "111", "adminSchools" => [ 0, 1, 2, 3, 4, 999999, ], "teacherSchools" => [ 1, 2, ], ];
OpenID Connect
$data = [ "sub" => "https://example.com/uri/parent/11111", "email_verified" => false, "persona" => "parent", // staff/teacher/parent/student "kid" => "JWT Signing (Internal)", "iss" => "https://example.com/oauth2/", "preferred_username" => "username", "given_name" => "Given", "nonce" => "rPWmHGhGcagFOTiD", "ps_uri" => "https://example.com/uri/parent/578000", "aud" => [ 0 => "37823263-d6f4-4781-8ccf-5b21ba085ca4", ], "ps_account_token" => "gi0ubGGVL871AhyevNb6lg==", "ps_dcid" => 578000, "auth_time" => 1618205362, "exp" => DateTimeImmutable { date: 2021-04-01 00:00:00.0 +00:00 }, "oid2" => "https://example.com/oid/guardian/username", "iat" => DateTimeImmutable @1618205362 { date: 2021-04-01 00:00:00.0 +00:00 }, "family_name" => "Family", "jti" => "74220be8-9c3b-4776-8543-157e7a9892a9", "email" => "first.last@example.com", ]