เนื่องด้วย มหาวิทยาลัยฯ มีความพยายามพลักดันให้ผู้พัฒนาระบบของมหาวิทยาลัยฯ/วิทยาเขต ปรับระบบในส่วนของการลงชื่อเข้าใช้งานระบบสารสนเทศต่าง ๆ ไปที่หน้าจอล็อกอินเดียวกัน เมื่อผู้ใช้งานต้องการใช้ระบบ dss.psu, docs.psu, tor.psu, sis.psu หรือระบบสารสนเทศอื่น ๆ ระบบเหล่านี้จะไม่มีหน้าจอ login เป็นของตนเอง แต่จะเป็นการใช้บริการ login จากระบบ oauth.psu.ac.th (PSU Passport) อีกต่อหนึ่ง หลังจาก login สำเร็จก็จะกลับไปใช้งานระบบได้ตามปกติ

ต่อมา มหาวิทยาลัยฯ ได้ปรับเงื่อนไขให้การเข้าถึงข้อมูลส่วนกลางต้องทำผ่าน API ที่เตรียมไว้ และการเข้าถึงข้อมูลส่วนบุคคลต้องมีการใช้ Access Token ร่วมด้วย ปัจจบันผู้ดูแลระบบ login ได้เตรียมระบบตัวใหม่ที่ตอบโจทย์ดังกล่าวไว้แล้วที่ psusso.psu.ac.th หลังจากที่ได้มีการทดลองใช้งานเบื้องต้น พอจะกล่าวได้ว่าระบบล็อกอินตัวใหม่ใช้เทคโนโลยี OpenID Connect + Single Sign-On (OAuth2)

OpenID Connect

OpenID คือ ระบบสมาชิกแบบเปิดที่ทำให้ผู้ใช้สามารถลงชื่อเข้าใช้ระบบต่าง ๆ หรือการล็อกอิน โดยผู้ใช้ไม่จำเป็นต้องสมัครสมาชิกใหม่ของแต่ละเว็บไซต์หรือระบบ เช่น ใช้บัญชี Google ในการ login เข้าใช้ Youtube, Gmail หรือการใช้บัญชี Facebook ในการเข้าระบบ Shopping ต่าง ๆ เป็นต้น สำหรับสภาพแวดล้อมของ ม.อ. สามารถเทียบได้กับบัญชี PSU Passport ที่เราใช้ในการเข้าถึงระบบสารสนเทศต่าง ๆ WiFi และ อื่น ๆ

SSO

SSO หรือ Single Sign-On หรือ การลงชื่อเข้าใช้ระบบแบบรวมศูนย์ คือ ผู้ใช้งานลงชื่อเข้าใช้งานระบบ หรือ ล็อกอิน เพียงครั้งเดียว แล้วสามารถเข้าใช้งานระบบต่าง ๆ ได้ โดยไม่ต้องลงชื่อเข้าใช้งานซ้ำอีก โดย SSO ตัวใหม่ของมหาวิทยาลัยฯ ทำงานภายใต้ Authorization Framework รุ่น 2 หรือ OAuth2

OAuth2

OAuth2 เป็นมาตรฐานหนึ่ง (ไม่ใช่เครื่องมือ) ของระบบยืนยันตัวตน (Authentication) และ การจัดการสิทธิ์ (Authorization) ที่จะทำให้ Client หรือ ระบบต่าง ๆ ติดต่อกับ Server ผ่านสิ่งที่เรียกว่า Access Token เพื่อเป็นการยกสิทธิ์ของเราให้ระบบใด ๆ สามารถเข้าถึงข้อมูลของเราได้

Authentik

จากการทดลองขอ Access Token ผ่าน Postman พบว่า Token ที่ได้มาจะเป็นแบบ Authentik คือ จะมีรายละเอียดในส่วนที่เป็น Access token และ ID token

ไฟล์ที่ใช้ศึกษาการขอ Token จากระบบ PSUSSO ผ่าน Postman สามารถ Download ได้ที่นี่ Json file แล้วนำไป Import ใน Postman > Workspaces > Collections และกำหนดค่า Client ID และ Client Secret ที่ได้จากการลงทะเบียน แล้วกดปุ่ม Get new Access Token จะเปิด Browser แล้วให้ login ด้วยบัญชี PSU Passport จะได้ token มาใช้งาน ให้ทดลองเปลี่ยน URL เป็น Get https://psusso.psu.ac.th:8443/application/o/userinfo/ แล้วกดปุ่ม Send หากการทำงานต่าง ๆ ถูกต้อง จะได้ข้อมูลส่วนตัวของบัญชี PSU Passport ที่ได้ login ไปก่อนหน้านี้

Registration

เพื่อปรับการล็อกอินของระบบสารสนเทศไปใช้ SSO ผู้พัฒนาต้องทำการลงทะเบียนขอใช้งานไปยังผู้ดูแลระบบ โดยปัจจุบันใช้วิธีการ Email ไปยังคุณณัฐวุฒิ วิจิตร์ (nattawut.w) เจ้าหน้าที่สำนักนวัตกรรมดิจิทัลและระบบอัจฉริยะ (DiiS) อาจกำหนด Subject เป็น ขอเชื่อมต่อ psu passport แบบ openid authentik พร้อมแจ้งรายละเอียดต่าง ๆ ดังนี้

  1. ชื่อ Server/Web
  2. วัตถุประสงค์ของ Server/Web
  3. ชื่อผู้ดูแล Server/Web
  4. redirect URL

หลังจากที่เจ้าหน้าที่รับเรื่องเรียบร้อย จะส่ง Client ID และ Client Secret กลับมาให้ (ทั้งนี้อาจได้ไฟล์ open-configuration มาด้วย)

Server flow

ในขั้นตอนศึกษาทดลองครั้งนี้อ้างอิงจากคู่มือ OpenID Connect ของ Google ในหัวข้อ Server flow (Server-Side, PHP) ประกอบด้วย 6 ขั้นตอน ดังนี้

  1. Create an anti-forgery state token
  2. Send an authentication request to Google
  3. Confirm the anti-forgery state token
  4. Exchange code for access token and ID token
  5. Obtain user information from the ID token
  6. Authenticate the user

1. Create an anti-forgery state token

ในขั้นตอนนี้จะมีการสร้างเลขสุ่มชุดหนึ่ง จะเรียกว่า state token เพื่อใช้ในการป้องกันการปลอมแปลง ให้จัดเก็บไว้ใน SESSION และจะนำมาใช้ในขั้นตอนการตรวจสอบภายหลัง

        $state = bin2hex(random_bytes(128 / 8));
        $_SESSION['state_code'] = $state;

2. Send an authentication request to Google

จัดเตรียม URL สำหรับติดต่อกับ authorization_endpoint (ดูได้จาก open-configuration) โดยค่าที่จำเป็นต้องส่งไปพร้อมกันในรูปแบบของ query string ประกอบด้วย

        $queryStrings = array(
            "response_type" => "code",
            "client_id" => { your client id },
            "scope" => openid profile email,
            "redirect_uri" => { redirect_uri },
            "state" => $state,
            "nonce" => "",
        );

        $qs = http_build_query($queryStrings);
        $authorization_endpoint = "https://psusso.psu.ac.th:8443/application/o/authorize/";
        $url = "$authorization_endpoint?$qs";
        header("location: $url");

* ในทางปฎิบัติมักไม่แสดงลิงค์นี้ให้ผู้ใช้เห็น แต่นิยมเรียก url login ของระบบ แล้ว redirect ออกไปด้วยคำสั่ง header()
* state เป็นค่าที่ได้จากขั้นตอนที่ 1
* scope ให้ดูค่าที่รองรับจากไฟล์ open-configuration โดยให้คั่นด้วยช่องว่าง 1 ช่อง

3. Confirm anti-forgery state token

หลังจาก redirect ไปยัง authorization_endpoint ผู้ใช้งานกรอก Username/Password ยืนยัน MFA หากการทำงานถูกต้อง บราวเซอร์ จะย้ายการทำงานกลับมาที่ redirect_uri ที่มาพร้อมกับ query string ค่า code และ state ให้ทำการตรวจสอบว่า state ที่ได้รับมาตรงกับที่เก็บไว้ใน SESSION หรือไม่ (ขั้นตอนที่ 1)

        $code = $this->input->get('code'), //a one-time authorization code
        $state = $this->input->get('state')
     
        if ($state != $_SESSION['state_code']) {
           echo '401: Invalid state parameter';
        }

4. Exchange code for access token and ID token

นำค่า code ที่ได้รับมาไปขอ Access token และ ID token ที่ token_endpoint (ดูได้จาก open-configuration) โดยกำหนดให้ส่งคำร้องขอแบบ POST เท่านั้น

        $headers = array(
            "Accept: application/json",
            "Content-Type: application/x-www-form-urlencoded",
        );


        $post_data = array(
            "grant_type" => "authorization_code",
            "code" => $code,
            "client_id" => $psu_openid_configs->client_id,
            "client_secret" => $psu_openid_configs->client_secret,
            "redirect_uri" => $psu_openid_configs->redirect_uri
        );
        $post_params = http_build_query($post_data, '', '&');

        $access_token_response = json_decode($this->fetchURL($token_endpoint, $post_params, $headers));

        $_SESSION['token'] = serialize($access_token_response);

หากการทำงานถูกต้องค่าในตัวแปร access_token_response จะมีค่าเหล่านี้ คือ

   [
     {
       access_token: "eyJhbGci............................L4gCX11wQ",
       refresh_token: "d82jd................................SZ03m0D",
       token_type: "Bearer",
       expires_in: "300",
       id_token: "eyJhbG......................vL3Byb2plY3QtY29wboaSZ81nA"
     },
   ]

5. Obtain user information from the ID token

การดึงข้อมูลบัญชีจาก PSUSSO จะใช้เพียง access_token ในส่วนของ Header ของคำร้องแบบ GET ที่เรียกไปยัง userinfo_endpoint ก็สามารถได้ข้อมูลตาม scope ที่กำหนดในขั้นตอนที่ 1 แล้ว

        $access_token_response = unserialize($_SESSION['token']);
        $headers = array("Authorization: Bearer $access_token");
        $userinfo_response = json_decode($this->fetchURL($userinfo_endpoint, null, $headers));

โครงสร้างข้อมูลที่ได้จาก PSUSSO เป็นดังนี้

    [
      {
        email: "username@psu.ac.th",
        name: "FirstName LastName",
        preferred_username: "username",
        username: "username",
        display_name: "Full Name (w/o title.)",
        display_name_th: "ชื่อ-สกุล (ไม่มีคำนำหน้า)",
        first_name: "FirstName",
        first_name_th: "ชื่อ",
        last_name: "LastName",
        last_name_th: "นามสกุล",
        position_th: "ตำแหน่งงาน",
        campus_th: "ชื่อวิทยาเขต (ไม่มีรหัส)",
        department_th: "ชื่อส่วนงาน (ไม่มีรหัส)",
        office_name_th: "ชื่อหน่วยงาน (ไม่มีรหัส)",
        groups: {
           0: "C03 Student PowerUsers",
           1: "ISD Staffs",
           2: "PK8 Temporary Users PowerUsers",
           3: "staff"
        },
        sub: "sasadasfsafasfsafsafsafasfasfdas"
      },

    ]

6. Authenticate the user

หลังจากได้ข้อมูลผู้ใช้งานจาก PSUSSO แล้ว สามารถนำค่าอ้างอิงเหล่านั้นไปทำงานกับฐานข้อมูลระบบต่อไป

7. Refresh Token

เมื่อเวลาผ่านไปสักระยะแล้วพยายามจะเรียกข้อมูลบัญชีในข้อ 5 อาจจะไม่ได้ข้อมูลอีกแล้ว คาดว่าเกิดจาก access_token เดิมหมดอายุ จำเป็นต้องร้องขอ access_token ใหม่ ซึ่งสามารถทำได้โดยใช้ refresh_token ที่ได้มาพร้อมกัน หลังจากได้ access_token ใหม่ก็จะสามารถเรียกข้อมูลบัญชีในข้อ 5 ได้อีกครั้ง

        $headers = array(
            "Accept: application/json",
            "Content-Type: application/x-www-form-urlencoded",
        );

        $post_data = array(
            "grant_type" => 'refresh_token',
            "refresh_token" => $refresh_token,
            "client_id" => $psu_openid_configs->client_id,
            "client_secret" => $psu_openid_configs->client_secret,
        );

        $post_params = http_build_query($post_data, '', '&');

        $new_access_token_response = json_decode($this->fetchURL($token_endpoint, $post_params, $headers));

        $_SESSION['token'] = serialize($new_access_token_response);

fetchURL

เป็นการนำโค้ดจาก https://github.com/jumbojett/OpenID-Connect-PHP/blob/v0.8.0/src/OpenIDConnectClient.php#L1017 มาปรับใช้งาน เพื่อให้ง่ายในการตั้งค่า curl สำหรับดึงข้อมูลที่ทำได้ทั้งเป็น GET และ POST โดยใช้เงื่อนไขจาก $post_body

    protected function fetchURL($url, $post_body = null, $headers = array())
    {

        $curl_handle = curl_init();
        curl_setopt($curl_handle, CURLOPT_URL, $url);

        if ($post_body != null) {
            curl_setopt($curl_handle, CURLOPT_POST, true);
            curl_setopt($curl_handle, CURLOPT_POSTFIELDS, $post_body);
        }
        // If we set some heaers include them
        if (count($headers) > 0) {
            curl_setopt($curl_handle, CURLOPT_HTTPHEADER, $headers);
        }

        // Include header in result? (0 = yes, 1 = no)
        curl_setopt($curl_handle, CURLOPT_HEADER, 0);
        curl_setopt($curl_handle, CURLINFO_HEADER_OUT, true);

        // Allows to follow redirect
        curl_setopt($curl_handle, CURLOPT_FOLLOWLOCATION, true);

        // Should cURL return or print out the data? (true = return, false = print)
        curl_setopt($curl_handle, CURLOPT_RETURNTRANSFER, true);

        // Timeout in seconds
        curl_setopt($curl_handle, CURLOPT_TIMEOUT, 60);

        // Download the given URL, and return output
        $output = curl_exec($curl_handle);

        if ($output === false) {
            die("Connection Failure");
        }

        // $info = curl_getinfo($curl_handle);
        // print_r($info);
        // print_r($info['request_header']);

        // curl_close($curl_handle);

        return $output;
    }

หากพบความผิดพลาดใด ๆ ในบทความ หรือ มีคำแนะนำเพิ่มิตม รบกวนเม้นท์แจ้งให้ทราบจะขอบคุณมาก

Next Topics

  • เตรียมศึกษาการนำ Access token ไปร้องขอข้อมูลส่วนบุคคลผ่าน PSU Open API
  • เตรียมศึกษาการ Refresh tokens => เพิ่มเนื้อหาเป็นข้อ 7 ในบทความนี้แล้ว
บันทึกการทดลองติดตั้ง Docker image for Nagios

Discussion

Leave a Comment