diff --git a/README.md b/README.md index f6ef1e5..e1ab34a 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ A paradigm shift in the registration and sign-in process, Affinidi Login is a game-changing solution for developers. With our revolutionary passwordless authentication solution your user's first sign-in doubles as their registration, and all the necessary data for onboarding can be requested during this streamlined sign-in/signup process. End users are in full control, ensuring that they consent to the information shared in a transparent and user-friendly manner. This streamlined approach empowers developers to create efficient user experiences with data integrity, enhanced security and privacy, and ensures compatibility with industry standards. -| Passwordless Authentication | Decentralised Identity Management | Uses Latest Standards | -|---|---|---| +| Passwordless Authentication | Decentralised Identity Management | Uses Latest Standards | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Offers a secure and user-friendly alternative to traditional password-based authentication by eliminating passwords and thus removing the vulnerability to password-related attacks such as phishing and credential stuffing. | Leverages OID4VP to enable users to control their data and digital identity, selectively share their credentials and authenticate themselves across multiple platforms and devices without relying on a centralised identity provider. | Utilises OID4VP to enhance security of the authentication process by verifying user authenticity without the need for direct communication with the provider, reducing risk of tampering and ensuring data integrity. | ## Introduction @@ -15,13 +15,13 @@ This package extends Socialite to enable passwordless authentication with the Af Learn more about Laravel Socialite [here](https://laravel.com/docs/10.x/socialite) **Quick Links** + 1. [Installation & Usage](#setup--run-application-from-playground-folder) 2. [Create Affinidi Login Configuration](#create-affinidi-login-configuration) 3. Affinidi Login Integration with [Sample Laravel project](#setup--run-application-from-playground-folder) 4. Affinidi Login Integration in [Fresh Laravel Project](#setup--run-application-from-playground-folder) 5. Affinidi Login Integration in [Existing Laravel Project](#setup--run-application-from-playground-folder) - ## Installation & Basic Usage To get started with Affinidi Socialite, follow these steps: @@ -54,7 +54,7 @@ public function boot(): void } ``` -# Authentication +## Authentication To authenticate users using an OAuth provider, you will need two routes: one for redirecting the user to the OAuth provider, and another for receiving the callback from the provider after authentication. @@ -81,6 +81,7 @@ Create the Login Configuration using [Affinidi Dev Portal](https://portal.affini **Important**: Safeguard the Client ID and Client Secret and Issuer; you'll need them for setting up your environment variables. Remember, the Client Secret will be provided only once. **Note**: By default Login Configuration will requests only `Email VC`, if you want to request email and profile VC, you can refer PEX query under (docs\loginConfig.json)[playground\example\docs\loginConfig.json] and execute the below affinidi CLI command to update PEX + ``` affinidi login update-config --id -f docs\loginConfig.json ``` @@ -89,17 +90,17 @@ affinidi login update-config --id -f docs\loginConfig.json Open the directory `playground/example` in VS code or your favorite editor - 1. Install the dependencies by executing the below command in terminal +1. Install the dependencies by executing the below command in terminal ``` composer install ``` - 2. Create the `.env` file in the sample application by running the following command +2. Create the `.env` file in the sample application by running the following command ``` cp .env.example .env ``` - 3. Create Affinidi Login Configuration as mentioned [here](#create-affinidi-login-configuration) - - 4. Update below environment variables in `.env` based on the auth credentials received from the Login Configuration created earlier: +3. Create Affinidi Login Configuration as mentioned [here](#create-affinidi-login-configuration) + +4. Update below environment variables in `.env` based on the auth credentials received from the Login Configuration created earlier: ``` PROVIDER_CLIENT_ID="" PROVIDER_CLIENT_SECRET="" @@ -111,55 +112,65 @@ Open the directory `playground/example` in VS code or your favorite editor PROVIDER_CLIENT_SECRET="xxxxxxxxxxxxxxx" PROVIDER_ISSUER="https://yyyy-yyy-yyy-yyyy.apse1.login.affinidi.io" ``` -5. Run the application +5. Run the application ``` php artisan serve ``` -6. Open the [http://localhost:8000/](http://localhost:8000/), which displays login page - **Important**: You might error on redirect URL mismatch if you are using `http://127.0.0.1:8000/` instead of `http://localhost:8000/`. -7. Click on `Affinidi Login` button to initiate OIDC login flow with Affinidi Vault +6. Open the [http://localhost:8000/](http://localhost:8000/), which displays login page + **Important**: You might error on redirect URL mismatch if you are using `http://127.0.0.1:8000/` instead of `http://localhost:8000/`. +7. Click on `Affinidi Login` button to initiate OIDC login flow with Affinidi Vault

- ## Integration Affinidi Login - Fresh Laravel Project If you want to start fresh without any base reference app, then you can follow the below steps ### Create Laravel Project + Before creating your first Laravel project, you should ensure that your local machine has `PHP` and `Composer` installed. 1. You may create a new Laravel project via the Composer `create-project` command + ``` composer create-project laravel/laravel example-app ``` **Note**: If you enounter any issue on creating project like `fileInfo`, then you may have enable the fileInfo extension in your `php.ini` file like below + ``` extension=fileinfo ``` + 2. After the project has been created, start Laravel's local development server using the Laravel's Artisan CLI `serve` command + ``` cd example-app - + php artisan serve ``` + 3. Once you have started the Artisan development server, your application will be accessible in your web browser at [http://localhost:8000](http://localhost:8000) **Note**: If you encounter an error on generating Key, then execute the below command which updates `APP_KEY` in your .env file and then run the app + ``` php artisan key:generate ``` ### Install Affinidi Socialite Provider + To get started with Socialite, use the Composer package manager to add the package to your project's dependencies 1. Install Affinidi Socialite Library + ``` composer require affinidi/laravel-socialite-affinidi ``` -2. Open `AppServiceProvider.php` file under `app\Providers`, and bootstrap the Affinidi driver to socialite class inside function `boot()`, the code should look like below + +2. Open `AppServiceProvider.php` file under `app\Providers`, and bootstrap the Affinidi driver to socialite class inside function `boot()`, the code should look like below + ``` public function boot(): void { @@ -168,7 +179,9 @@ public function boot(): void \Affinidi\SocialiteProvider\AffinidiSocialite::extend($socialite); } ``` + 3. Add credentials for the Affinidi OIDC provider, should be placed in your application's `config/services.php` configuration file, + ``` 'affinidi' => [ 'base_uri' => env('PROVIDER_ISSUER'), @@ -185,15 +198,14 @@ public function boot(): void 3. Create file `login.blade.php` under `resources\views` for adding Affinidi Login button, reference can be found [here](playground\example\resources\views\login.blade.php) 4. Create dashboard `dashboard.blade.php` under `resources\views` for displaying the logged in user info, reference can be found [here](playground\example\resources\views\dashboard.blade.php) - ### Run the application 1. Run the application - ``` - php artisan serve - ``` -2. Open the [http://localhost:8000/](http://localhost:8000/), which displays login page - **Important**: You might error on redirect URL mismatch if you are using `http://127.0.0.1:8000/` instead of `http://localhost:8000/`. + ``` + php artisan serve + ``` +2. Open the [http://localhost:8000/](http://localhost:8000/), which displays login page + **Important**: You might error on redirect URL mismatch if you are using `http://127.0.0.1:8000/` instead of `http://localhost:8000/`. 3. Click on `Affinidi Login` button to initiate OIDC login flow with Affinidi Vault
@@ -205,13 +217,17 @@ public function boot(): void If you want to integrate Affinidi Login to any existing PHP Laravel Application using socialite, then you can follow the below steps ### Install Affinidi Socialite Provider + To get started with Socialite, use the Composer package manager to add the package to your project's dependencies 1. Install Affinidi Socialite Library + ``` composer require affinidi/laravel-socialite-affinidi ``` -2. Open `AppServiceProvider.php` file under `app\Providers`, and bootrap the affinidi driver to socialite class inside function `boot()`, the code should look like below + +2. Open `AppServiceProvider.php` file under `app\Providers`, and bootrap the affinidi driver to socialite class inside function `boot()`, the code should look like below + ``` public function boot(): void { @@ -220,7 +236,9 @@ public function boot(): void \Affinidi\SocialiteProvider\AffinidiSocialite::extend($socialite); } ``` + 3. Add credentials for the Affinidi OIDC provider, should be placed in your application's `config/services.php` configuration file, + ``` 'affinidi' => [ 'base_uri' => env('PROVIDER_ISSUER'), @@ -229,6 +247,7 @@ public function boot(): void 'redirect' => '/login/affinidi/callback', ], ``` + 4. Create the Login Configuration as per step [here](#create-affinidi-login-configuration) 5. Update below environment variables in .env based on the auth credentials obtained from the previous step @@ -237,18 +256,88 @@ PROVIDER_CLIENT_ID="" PROVIDER_CLIENT_SECRET="" PROVIDER_ISSUER="" -6. Add the Affinidi Login button in your login page, reference can be found [here]((playground\example\resources\views\login.blade.php)) +6. Add the Affinidi Login button in your login page, reference can be found [here](<(playground\example\resources\views\login.blade.php)>) 7. Use socialite driver as 'affinidi' in route handler / controller, reference controller can be found [here](playground\example\app\Http\Controllers\LoginRegisterController.php) ### Run the application 1. Run the application - ``` - php artisan serve - ``` -2. Open the [http://localhost:8000/](http://localhost:8000/), which displays login page - **Important**: You might error on redirect URL mismatch if you are using `http://127.0.0.1:8000/` instead of `http://localhost:8000/`. + ``` + php artisan serve + ``` +2. Open the [http://localhost:8000/](http://localhost:8000/), which displays login page + **Important**: You might error on redirect URL mismatch if you are using `http://127.0.0.1:8000/` instead of `http://localhost:8000/`. 3. Click on `Affinidi Login` button to initiate OIDC login flow with Affinidi Vault +## Call Affinidi APIs +For example, if you want to issue a VC + +- Generate Personal access token using command line tool more details [here]() and update .env file with details + +``` +VAULT_URL="https://vault.affinidi.com" +API_GATEWAY_URL="https://apse1.api.affinidi.io" +TOKEN_ENDPOINT="https://apse1.auth.developer.affinidi.io/auth/oauth2/token" +PROJECT_ID="" +KEY_ID="" +TOKEN_ID="" +PASSPHRASE="" +PRIVATE_KEY="" +``` + +- Set the service config file + +``` +'affinidi_tdk' => [ + 'api_gateway_url' => env('API_GATEWAY_URL'), + 'token_endpoint' => env('TOKEN_ENDPOINT'), + 'project_Id' => env('PROJECT_ID'), + 'private_key' => env('PRIVATE_KEY'), + 'token_id' => env('TOKEN_ID'), + 'passphrase' => env('PASSPHRASE'), + 'key_id' => env('KEY_ID'), + 'vault_url' => env('VAULT_URL'), +], +``` + +- Code snippet to invoke TDK helper methods by reading config values + +``` + $credentials_request = + [ + [ + "credentialTypeId" => "AnyTcourseCertificateV1R0", + "credentialData" => [ + "courseID" => "EMP-IT-AUTOMATION-2939302", + "course" => [ + "name" => "IT Automation with Python", + "type" => "Professional Certificate", + "url" => "", + "courseDuration" => "45 Days" + ], + "learner" => [ + "name" => "", + "email" => "grajesh.c@affinidi.com", + "phone" => "" + ], + "achievement" => [ + "score" => "100", + "grade" => "A" + ], + "courseMode" => "online", + "completionDate" => "08/09/2024" + ] + ] + ]; + + + $apiMethod = '/cis/v1/' . config('services.affinidi_tdk.project_Id') . '/issuance/start'; + + $data = \Affinidi\SocialiteProvider\AffinidiTDK::InvokeAPI($apiMethod, [ + 'data' => $credentials_request, + 'claimMode' => "TX_CODE" + ]); + +``` diff --git a/composer.json b/composer.json index 8f32f56..9444e3b 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,9 @@ }, "require": { "php": "^8.0", - "laravel/socialite": "^5.10" + "laravel/socialite": "^5.10", + "firebase/php-jwt": "^6.10", + "guzzlehttp/guzzle": "^7.8" }, "license": "MIT", "autoload": { diff --git a/composer.lock b/composer.lock index bb42d44..eca2428 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c67a2ea917ba0db6101af2f9e9bb28e8", + "content-hash": "b95223b963f983eb9b6ccc5af122ff03", "packages": [ { "name": "doctrine/inflector", @@ -97,6 +97,69 @@ ], "time": "2023-06-16T13:40:37+00:00" }, + { + "name": "firebase/php-jwt", + "version": "v6.10.1", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "500501c2ce893c824c801da135d02661199f60c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/500501c2ce893c824c801da135d02661199f60c5", + "reference": "500501c2ce893c824c801da135d02661199f60c5", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v6.10.1" + }, + "time": "2024-05-18T18:05:11+00:00" + }, { "name": "fruitcake/php-cors", "version": "v1.3.0", @@ -170,16 +233,16 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.8.0", + "version": "7.8.1", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9" + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/1110f66a6530a40fe7aea0378fe608ee2b2248f9", - "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104", + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104", "shasum": "" }, "require": { @@ -194,11 +257,11 @@ "psr/http-client-implementation": "1.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.1", + "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.29 || ^9.5.23", + "phpunit/phpunit": "^8.5.36 || ^9.6.15", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -276,7 +339,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.8.0" + "source": "https://github.com/guzzle/guzzle/tree/7.8.1" }, "funding": [ { @@ -292,7 +355,7 @@ "type": "tidelift" } ], - "time": "2023-08-27T10:20:53+00:00" + "time": "2023-12-03T20:35:24+00:00" }, { "name": "guzzlehttp/promises", @@ -3280,5 +3343,5 @@ "php": "^8.0" }, "platform-dev": [], - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } diff --git a/src/AffinidiTDK.php b/src/AffinidiTDK.php new file mode 100644 index 0000000..48e78db --- /dev/null +++ b/src/AffinidiTDK.php @@ -0,0 +1,151 @@ + 'Bearer ' . $pst, + 'Content-Type' => 'application/json', + ])->post($apiUrl, $data); + + $responseJson = $response->json(); + Log::info('Response: ' . json_encode($data)); + + return $responseJson; + + } + + public static function fetchProjectScopedToken(): string + { + //Check PST is available in file + $tokenFilePath = 'pst_token.txt'; + if (file_exists($tokenFilePath)) { + $tokenData = file_get_contents($tokenFilePath); + if (!AffinidiTDK::isTokenExpired($tokenData)) { + Log::info('Project Scope Token already exists and its valid ' . $tokenData); + return $tokenData; + } + } + //Token not exists or expired, so generating new PST + Log::info('Generating PST'); + + $tdkConfig = config('services.affinidi_tdk'); + + $userToken = AffinidiTDK::getUserAccessToken($tdkConfig); + + Log::info('User Access Token: ' . $userToken); + + $api_gateway_url = $tdkConfig['api_gateway_url']; + $project_Id = $tdkConfig['project_Id']; + + $projectTokenEndpoint = $api_gateway_url . '/iam/v1/sts/create-project-scoped-token'; + + $response = Http::withHeaders([ + 'Authorization' => 'Bearer ' . $userToken, + 'Content-Type' => 'application/json', + ])->post($projectTokenEndpoint, [ + 'projectId' => $project_Id + ]); + + Log::info('Response: ' . $response->body()); + $responseData = $response->json(); + Log::info('Parsed Response Data: ' . json_encode($responseData)); + + if (!isset($responseData['accessToken'])) { + Log::error('Access token not found in response: ' . json_encode($responseData)); + $error = new \Error('Access token not found while generating project scope token'); + throw $error; + } + + Log::info('Access token found in response: ' . $responseData['accessToken']); + $pst = $responseData['accessToken']; + + file_put_contents($tokenFilePath, $pst); + return $pst; + + } + private static function isTokenExpired($token): bool + { + list($header, $payload, $signature) = explode('.', $token); + + $payload = json_decode(base64_decode($payload), true); + if (isset($payload['exp'])) { + $currentTimestamp = time(); + return $currentTimestamp >= $payload['exp']; + } + + // If no exp claim, assume expired + return true; + } + + private static function getUserAccessToken($tdkConfig) + { + $token_endpoint = $tdkConfig['token_endpoint']; + $private_key = $tdkConfig['private_key']; + $token_id = $tdkConfig['token_id']; + $key_id = isset($tdkConfig['key_id']) ? $tdkConfig['key_id'] : $tdkConfig['token_id']; + $passphrase = isset($tdkConfig['passphrase']) ? $tdkConfig['passphrase'] : null; + + $algorithm = 'RS256'; + $issueTimeS = floor(time()); + // Generate a unique jti value + $jti = (string) \Str::uuid(); + $payload = [ + 'iss' => $token_id, + 'sub' => $token_id, + 'aud' => $token_endpoint, + 'jti' => $jti, + 'iat' => $issueTimeS, + 'exp' => $issueTimeS + 5 * 60 + ]; + + $headers = [ + 'kid' => $key_id, + ]; + Log::info('Payload: ' . json_encode($payload)); + + $key = openssl_pkey_get_private($private_key, $passphrase); + + $token = JWT::encode($payload, $key, $algorithm, $key_id, $headers); + + Log::info('Token: ' . $token); + + $response = Http::withHeaders([ + 'Content-Type' => 'application/x-www-form-urlencoded', + ])->asForm()->post($token_endpoint, [ + 'grant_type' => 'client_credentials', + 'scope' => 'openid', + 'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', + 'client_assertion' => $token, + 'client_id' => $token_id + ]); + + Log::info('Response: ' . $response->body()); + + $responseData = $response->json(); + + if (isset($responseData['access_token'])) { + return $responseData['access_token']; + } else { + Log::error('Access token not found in response: ' . json_encode($responseData)); + $error = new \Error('Access token not found while generating user access token'); + throw $error; + } + + } +}