Xây dựng API login sử dụng các dịch vụ mạng xã hội: Facebook, Twitter, Google

Đặt vấn đề

Ngày nay khi sử dụng một web/app ta không còn xa lạ gì với việc có thể đăng nhập vào hệ thống bằng nhiều cách khác nhau. Cách truyền thống là login bằng email hoặc username kết hợp cùng password. Và một cách khác là đăng nhập sử dụng xác thực với một bên thứ 3 ví dụ như là Facebook, Twitter, Google... Lý do để tạo ra nhiều cách đăng nhập như thế này thứ nhất là để thêm phương án lựa chọn cho người dùng thứ 2 là để người dùng có thể tận dụng các tài khoản mạng xã hội để đăng nhập vào hệ thống mà không cần phải nhớ quá nhiều tài khoản, vừa tiện dụng vừa đảm bảo an toàn bảo mật. Vậy thì hôm nay chúng ta sẽ cùng tìm hiểu cách để tạo các API để đăng nhập bằng các dịch vụ bên thứ 3: Facebook, Twitter, Google... Let's go!

Tìm hiểu cơ chế

Có rất nhiều cách để thiết kế API nhưng phổ biến và được dùng nhiều nhất là RESTful API. Vì RESTful API không sử dụng session và cookies nên để authentication ta sử dụng đến JSON Web Token. Để hiểu rõ hơn về RESTful API và JWT thì trên viblo đã có rất nhiều bài viết về chủ đề này, các bạn có thể tìm hiểu thêm. Trong phạm vi bài viết này mình sẽ sử dụng RESTful API và JWT để xây dựng API login với Facebook, Twitter và Google. Trước hết ta cần hiểu cơ chế authen của JWT, về cơ bản và dễ hiểu nhất mình xin phép quote đoạn kịch bản authen sau:

Authentication: Đây là kịch bản phổ biến nhất cho việc sử dụng JWT. Một khi người dùng đã đăng nhập vào hệ thống thì những request tiếp theo từ phía người dùng sẽ chứa thêm mã JWT, cho phép người dùng quyền truy cập vào các đường dẫn, dịch vụ, và tài nguyên mà cần phải có sự cho phép nếu có mã Token đó. Phương pháp này không bị ảnh hưởng bởi Cross-Origin Resource Sharing (CORS) do nó không sử dụng cookie.

Mình xin được mượn tạm hình ảnh về flow login sử dụng jwt trên trang chủ của nó (Link) để giải thích một chút về kịch bản vừa quote ở trên 😄. Tạm giải thích như sau: Browser chính là Client, khi Client gửi request đăng nhập lên server có chứa username/email và password thì server sẽ tạo ra một jwt và trả về cho Client. Kể từ đó khi Client muốn truy cập vào tài nguyên nào cần phải xác thực thì trong request đó cần đính kèm header Authorization chính là cái token đã được trả về từ trước đó. Server nhận request check xem token hợp lệ thì thực hiện yêu cầu của request và trả lại kết quả cho Client. Vâng ở trên chính là flow authen dành cho trường hợp truyền thống sử dụng username/email + password để đăng nhập. Bây giờ ta cần chỉnh sửa một chút để áp dụng với đăng nhập bên thứ 3. Có thể mường tượng nó như thế này: Đầu tiên ta cần authen với bên thứ 3 (Facebook, Twitter, Google) bằng tài khoản của dịch vụ đó và lấy đc access_token. Gửi access_token đó lên server, server gửi request lấy thông tin user dùng access_token của bên thứ 3 đó. Trong thông tin mà API bên thứ 3 trả về sẽ chứa userIdSocial, từ userIdSocial này ta check xem đã có trong hệ thống của mình chưa nếu userIdSocial chưa có thì mình sẽ tạo 1 user mới lấy những thông tin từ bên thứ 3 về để điền vào ví dụ như là name, email, .... sau khi tạo user thì server cũng tạo luôn jwt và trả về cho client, còn nếu userIdSocial đã có trong hệ thống thì mình chỉ cần tạo ra jwt và trả về cho client, flow tiếp theo tương tự như với trường hợp bên trên. Như vậy chỉ cần lấy đc access_token thì ta có thể đăng nhập vào hệ thống mà không cần password rồi. 😉 Đã hiểu được cơ chế của nó thì mình bắt tay vào code thôi.. (go)

Xây dựng API

Đầu tiên để xây dựng được API login bằng mạng xã hội ta cần trải qua một bước tạm gọi là "phân tích thiết kế". Việc cần làm là vẽ ra cái Database đã. Ta có một bảng users chứa các thông tin về người dùng: email, password, name. Mỗi user có thể liên kết với nhiều mạng xã hội khác nhau vì vậy ta sẽ thiết kế thêm 1 bảng nữa tạm gọi là social_networks có quan hệ 1 nhiều với bảng users và có chứa các trường user_id, type, social_id. Migration như sau:

Schema::create('social_networks', function (Blueprint $table) {
    $table->increments('id');
    $table->integer('user_id');
    $table->tinyInteger('type'); // 1: facebook; 2:twitter; 3:google
    $table->string('social_id')->nullable();
    $table->timestamps();
});

API login với Facebook

Đầu tiên ta tải thư viện graph của facebook

composer require facebook/graph-sdk

Ta sẽ code giống như flow đã vẽ ra ở trên. Các bạn cùng xem code nhé, mình có comment khá dễ hiểu rồi 😄

// app\Http\Controllers\Api\AuthController.php
// use Facebook\Facebook;
public function facebook(Request $request)
{
    $facebook = $request->only('access_token');
    if (!$facebook || !isset($facebook['access_token'])) {
        return $this->responseErrors(config('code.user.login_facebook_failed'), trans('messages.user.login_facebook_failed'));
    }
    // Khởi tạo instance của Facebook Graph SDK
    $fb = new Facebook([
        'app_id' => config('services.facebook.app_id'),
        'app_secret' => config('services.facebook.app_secret'),
    ]);

    try {
        $response = $fb->get('/me?fields=id,name,email,link,birthday', $facebook['access_token']); // Lấy thông tin 
        // user facebook sử dụng access_token được gửi lên từ client
        $profile = $response->getGraphUser();
        if (!$profile || !isset($profile['id'])) { // Nếu access_token không lấy đc thông tin hợp lệ thì trả về login false luôn
            return $this->responseErrors(config('code.user.login_facebook_failed'), trans('messages.user.login_facebook_failed'));
        }

        $email = $profile['email'] ?? null;
        $social = SocialNetwork::where('social_id', $profile['id'])->where('type', config('user.social_network.type.facebook'))->first();
        // Lấy được userId của Facebook ta kiểm tra trong bảng social_networks đã có chưa, nếu có thì tài khoản facebook này 
		// đã từng đăng nhập vào hệ thống ta chỉ cần lấy ra user rồi generate jwt trả về cho client; Ngược lại nếu chưa có thì 
        // ta sẽ tiếp tục dùng email trả về từ facebook kiểm tra xem nếu có user với email như thế rồi thì lấy luôn user đó nếu 
        // không thì tạo user mới với email trên và tạo bản ghi social_network lưu thông tin userId của facebook rồi generate jwt
        // để trả về cho client
        if ($social) {
            $user = $social->user;
        } else {
            $user = $email ? User::firstOrCreate(['email' => $email]) : User::create();
            $user->socialNetwork()->create([
                'social_id' => $profile['id'],
                'type' => config('user.social_network.type.facebook'),
            ]);
            $user->name = $profile['name'];
            $user->save();
        }

        $token = JWTAuth::fromUser($user);

        return $this->responseSuccess(compact('token', 'user'));
    } catch (\Exception $e) {
        Log::error('Error when login with facebook: ' . $e->getMessage());
        return $this->responseErrors(config('code.user.login_facebook_failed'), trans('messages.user.login_facebook_failed'));
    }
}

Giờ việc còn lại chỉ cần tạo route trỏ tới hàm trên nữa là done.

// route\api.php
Route::post('user/login/facebook', '[email protected]');

API login với Twitter

Với Twitter ta sẽ sử dụng thư viện abraham/twitteroauth để fetch thông tin user từ Twitter Code tương tự như với facebook, chỉ có khác là với twitter ta sẽ nhận tới 2 tham số token là access_tokenaccess_token_secret

// app\Http\Controllers\Api\AuthController.php
// use Abraham\TwitterOAuth\TwitterOAuth;
public function twitter(Request $request)
{
    $twitter = $request->only('access_token', 'access_token_secret');
    if (!$twitter || !isset($twitter['access_token']) || !isset($twitter['access_token_secret'])) {
        return $this->responseErrors(config('code.user.login_twitter_failed'), trans('messages.user.login_twitter_failed'));
    }

    $tw = new TwitterOAuth(
        config('services.twitter.consumer_key'),
        config('services.twitter.consumer_secret'),
        $twitter['access_token'],
        $twitter['access_token_secret']
    );
    $tw->setDecodeJsonAsArray(true);
    try {
        $response = $tw->get('account/verify_credentials');
        if (isset($response['errors'])) {
            return $this->responseErrors(config('code.user.login_twitter_failed'), trans('messages.user.login_twitter_failed'));
        }

        $social = SocialNetwork::where('social_id', $response['id_str'])->where('type', config('user.social_network.type.twitter'))->first();
        if ($social) {
            $user = $social->user;
        } else {
            $user = User::create([
                'name' => $response['name'],
            ]);
            $user->socialNetwork()->create([
                'social_id' => $response['id_str'],
                'type' => config('user.social_network.type.twitter'),
            ]);
        }

        $token = JWTAuth::fromUser($user);

        return $this->responseSuccess(compact('token', 'user'));
    } catch (\Exception $e) {
        Log::error('Error when login with twitter: ' . $e->getMessage());
        return $this->responseErrors(config('code.user.login_twitter_failed'), trans('messages.user.login_twitter_failed'));
    }
}

Thêm route:

Route::post('user/login/twitter', '[email protected]');

API login với Google

Với google thì ta sẽ sử dụng google/apiclient để verify token và fetch thông tin user. Và client sẽ gửi lên id_token khi client thực hiện đăng nhập với Google Sign-In.

// app\Http\Controllers\Api\AuthController.php
public function google(Request $request)
{
    $idToken = $request->get('id_token');
    if (!$idToken) {
        return $this->responseErrors(config('code.user.login_google_failed'), trans('messages.user.login_google_failed'));
    }

    try {
        $client = new \Google_Client(['client_id' => config('services.google.client_id')]);
        $payload = $client->verifyIdToken($idToken);
        if (!$payload) {
            return $this->responseErrors(config('code.user.login_google_failed'), trans('messages.user.login_google_failed'));
        }

        $social = SocialNetwork::where('social_id', $payload['sub'])->where('type', config('user.social_network.type.google'))->first();
        if ($social) {
            $user = $social->user;
        } else {
            $email = $payload['email'] ?? null;
            $user = $email ? User::firstOrCreate(['email' => $email]) : User::create();
            $user->name = $payload['name'];
            $user->save();
            $user->socialNetwork()->create([
                'social_id' => $payload['sub'],
                'type' => config('user.social_network.type.google'),
            ]);
        }

        $token = JWTAuth::fromUser($user);

        return $this->responseSuccess(compact('token', 'user'));
    } catch (\Exception $e) {
        Log::error('Error when login with google: ' . $e->getMessage());
        return $this->responseErrors(config('code.user.login_google_failed'), trans('messages.user.login_google_failed'));
    }
}

Thêm route:

Route::post('user/login/google', '[email protected]');

Kết

Như vậy mình vừa xây dựng đủ 3 API login với 3 dịch vụ ta vẫn thường hay dùng mỗi khi phát triển web hoặc app. Hi vọng bài viết có ích với các bạn. Các bạn có thể tham khảo code ở link Github: https://github.com/quanvhframgia/api-login-socials Link: