grantholle/laravel-powerschool-auth

使用PowerSchool验证您的Laravel应用程序

3.1.0 2024-03-28 16:54 UTC

This package is auto-updated.

Last update: 2024-08-28 17:53:06 UTC


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流程支持四种类型(staffteacherparentstudent),但为了灵活性,staffteacher将合并为单个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 登录页面。您还可以通过添加一个具有真值(如 1true)的 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",
]

许可证

MIT