Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OAuth2.0 #4102

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
Draft

OAuth2.0 #4102

wants to merge 16 commits into from

Conversation

hirale
Copy link
Contributor

@hirale hirale commented Jul 18, 2024

  1. Create a new client in admin panel System > Web Services > REST - OAuth2 Clients
Client ID Client Name Client Secret (Auto-generated) Redirect URI Grant Types
1 Test Client 30e095dedb4ccb40d42a9e5bd4074b55209e6565 https://example.com/redirect authorization_code, refresh_token
  1. Authorization

    • Authorization code (interactive)
      This method works only for customers.
      Open the browser and navigate to this URL: https://example.com/oauth2/authorize?client_id=1&redirect_uri=https://example.com/redirect

      if customer is not logged in, customer will be redirected to login page, then navigate to this URL: https://example.com/oauth2/authorize?client_id=1&redirect_uri=https://example.com/redirect

      Then customer need to clicck Allow button.

      Browser will be redirected to redirect_uri with authorization code: https://localhost/redirect?code=5a940d320d1dd5cd7eb6faf8929dfab9f3b61b38

      Then go next step to get access token

    • Device code (headless)

      1. Request a new device code

        curl  -X POST \
        'https://example.com/oauth2/device/request' \
        --header 'Content-Type: application/x-www-form-urlencoded' \
        --data-urlencode 'client_id=4' \
        {
            "code": 200,
            "message": "Success",
            "data": {
                "device_code": "46697f91401cd8",
                "user_code": "4A5CBE",
            }
        }
      2. Authorize the device

          curl  -X POST \
          'https://example.com/oauth2/device/authorize' \
          --header 'Content-Type: application/x-www-form-urlencoded' \
          --data-urlencode 'user_code=4A5CBE'
          --data-urlencode 'client_secret=30e095dedb4ccb40d42a9e5bd4074b55209e6565'
          --data-urlencode 'client_id=4'
          --data-urlencode 'user_type=admin' # customer or admin
          --data-urlencode 'id=1' # customer id or amdin id
          --data-urlencode '[email protected]' # customer email or admin email
          {
              "code": 200,
              "message": "Success",
          }
      1. Get the access token

        curl  -X POST \
        'https://example.com/oauth2/device/poll' \
        --header 'Content-Type: application/x-www-form-urlencoded' \
        --data-urlencode 'device_code=46697f91401cd8'
        --data-urlencode 'user_type=admin' # customer or admin
        --data-urlencode 'id=1' # customer id or amdin id
        {
            "code": 200,
            "message": "Success",
            "data": {
                "access_token": "6c215ba6fe574b7556dc338cfd10993bf3b61b38",
                "token_type": "Bearer",
                "expires_in": "1721237670",
                "refresh_token": "018cd2dcb5a0d2b88f5314be3872f18af3b61b38"
            }
        }
  2. Obtain access token

    • Authorization code

      curl  -X POST \
      'https://example.com/oauth2/token' \
      --header 'Content-Type: application/x-www-form-urlencoded' \
      --data-urlencode 'grant_type=authorization_code' \
      --data-urlencode 'code=5a940d320d1dd5cd7eb6faf8929dfab9f3b61b38' \
      --data-urlencode 'client_id=4' \
      --data-urlencode 'redirect_uri=https://example.com/redirect' \
      --data-urlencode 'client_secret=30e095dedb4ccb40d42a9e5bd4074b55209e6565'
      {
          "code": 200,
          "message": "Success",
          "data": {
              "access_token": "858349ebf33c605966c17a3f200d438bf3b61b38",
              "token_type": "Bearer",
              "expires_in": 1721236965,
              "refresh_token": "3aa5795e07b9493a43392f64edf6a70cf3b61b38"
          }
      }
    • Refresh token

      curl  -X POST \
      'https://example.com/oauth2/token' \
      --header 'Content-Type: application/x-www-form-urlencoded' \
      --data-urlencode 'grant_type=refresh_token' \
      --data-urlencode 'refresh_token=3aa5795e07b9493a43392f64edf6a70cf3b61b38' \
      --data-urlencode 'client_id=4' \
      --data-urlencode 'redirect_uri=https://example.com/redirect' \
      --data-urlencode 'client_secret=30e095dedb4ccb40d42a9e5bd4074b55209e6565'
      {
          "code": 200,
          "message": "Success",
          "data": {
              "access_token": "e0ee8089a0cd6f5024c2969b8715e5e6f3b61b38",
              "token_type": "Bearer",
              "expires_in": 1721237684,
              "refresh_token": "528951a74705b6a53ef6991d0aab2aa0f3b61b38"
          }
      }

Test the token

curl  -X GET \
  'https://example.com/api/rest/products?limit=10&page=1' \
  --header 'Accept-Encoding: application/json' \
  --header 'Authorization: Bearer e0ee8089a0cd6f5024c2969b8715e5e6f3b61b38'
{
  "231": {
    "entity_id": "231",
    "type_id": "simple",
    "sku": "msj000",
    "created_at": "2013-03-05 05:48:12",
    "updated_at": "2024-05-16 19:10:34",
    "name": "French Cuff Cotton Twill Oxford",
    "meta_title": null,
    "meta_description": null,
    "description": "Button front. Long sleeves. Tapered collar, chest pocket, french cuffs.",
    "meta_keyword": null,
    "short_description": "Made with wrinkle resistant cotton twill, this French-cuffed luxury dress shirt is perfect for Business Class frequent flyers.",
    "color": "22",
    "occasion": "30",
    "apparel_type": "41",
    "style": null,
    "size": "80",
    "length": null,
    "gender": "93",
    "regular_price_with_tax": 190,
    "regular_price_without_tax": 190,
    "final_price_with_tax": 190,
    "final_price_without_tax": 190,
    "is_saleable": true,
    "image_url": "https://example.com/media/catalog/product/cache/0/image/9df78eab33525d08d6e5fb8d27136e95/m/s/msj000t_1.jpg"
  },
  "232": {
    "entity_id": "232",
    "type_id": "simple",
    "sku": "msj001",
    "created_at": "2013-03-05 05:48:13",
    "updated_at": "2024-05-16 19:10:34",
    "name": "French Cuff Cotton Twill Oxford",
    "meta_title": null,
    "meta_description": null,
    "description": "Button front. Long sleeves. Tapered collar, chest pocket, french cuffs.",
    "meta_keyword": null,
    "short_description": "Made with wrinkle resistant cotton twill, this French-cuffed luxury dress shirt is perfect for Business Class frequent flyers.",
    "color": "22",
    "occasion": "30",
    "apparel_type": "41",
    "style": null,
    "size": "79",
    "length": null,
    "gender": "93",
    "regular_price_with_tax": 190,
    "regular_price_without_tax": 190,
    "final_price_with_tax": 190,
    "final_price_without_tax": 190,
    "is_saleable": true,
    "image_url": "https://example.com/media/catalog/product/cache/0/image/9df78eab33525d08d6e5fb8d27136e95/m/s/msj000t_1.jpg"
  }
}

@github-actions github-actions bot added Template : base Relates to base template Component: Api2 Relates to Mage_Api2 labels Jul 18, 2024
@hirale
Copy link
Contributor Author

hirale commented Jul 18, 2024

Any suggestions?

@colinmollenhour
Copy link
Member

Excellent work, Hirale!

Regarding reviewing this feature, it is a lot of work and we are not all OAuth 2.0 experts. I'm sure you did a lot of testing and put a ton of thought into it as you went, can you perhaps provide more of that info here? For example:

  • What specific security concerns did you have and how did you mitigate them?
  • Are there any resources you used as a blueprint? Perhaps sharing those will help with some context for others trying to understand.
  • Is there a reference implementation that we can easily set up and test? Or perhaps a "playground" of some sort? The curl commands and example responses are very helpful but if we want to actually prove it works with a real system it would be nice not to have to create an implementation from scratch.

This goes for customer vs admin as well, I skimmed the code and am not sure I understand that separation well. Maybe I'm just ignorant on this, I never used M2's OAuth implementation, is it inspired by how M2 does it?

@hirale
Copy link
Contributor Author

hirale commented Jul 23, 2024

I developed this module because there is no headless mode for REST authentication in OpenMage.
This module has two authentication methods, you can check GitHub OAuth for more details:

  1. Standard OAuth2 Flow: Using the authorization code.
  2. Device Code Flow: For headless applications.

At the beginning, I completely imitated the authorization method in the GitHub OAuth Device Flow, but the user needs to authorize the device code from the background. Since the background of OpenMage requires login, this did not meet the need for automatically obtaining tokens in headless mode. Therefore, I changed it to use the same interface for both customer and admin to authorize, adding a user_type parameter to distinguish users and verify user information.

I’m open to feedback on whether this approach is appropriate or if there are better alternatives. If you have a better idea, please let me know.

QQ_1721725276417

  1. Authorization Code Flow
<?php
// Step 1: Redirect to the authorization URL
$client_id = '6';
$redirect_uri = 'https://yourapplication.com/redirect';
$authorization_url = "https://example.com/oauth2/authorize?client_id=$client_id&redirect_uri=$redirect_uri";

echo "Go to the following URL to authorize:\n$authorization_url\n\n";
echo "After authorizing, enter the authorization code you received:\n";

// Step 2: Prompt the user to enter the authorization code
$authorization_code = trim(fgets(STDIN));

// Step 3: Exchange authorization code for access token
$client_secret = '05840bc55ccf415ef437a88f02ecce6eb4207023';

$ch = curl_init('https://example.com/oauth2/token');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
    'grant_type' => 'authorization_code',
    'code' => $authorization_code,
    'client_id' => $client_id,
    'redirect_uri' => $redirect_uri,
    'client_secret' => $client_secret
]));
//curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
//curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

$response = curl_exec($ch);
curl_close($ch);

$token_info = json_decode($response, true);

if (isset($token_info['data']['access_token'])) {
    $access_token = $token_info['data']['access_token'];
    $refresh_token = $token_info['data']['refresh_token'];

    echo "Access Token: $access_token\n";
    echo "Refresh Token: $refresh_token\n";

    // Step 4: Test the token
    $ch = curl_init('https://example.com/api/rest/products?limit=10&page=1');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Accept-Encoding: application/json',
        "Authorization: Bearer $access_token"
    ]);
    //curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    //curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

    $response = curl_exec($ch);
    curl_close($ch);

    echo "Products: $response\n";
} else {
    echo "Failed to obtain access token. Response: $response\n";
}
?>
  1. Device Code Authorization Flow
<?php
$client_id = '4';
$client_secret = '30e095dedb4ccb40d42a9e5bd4074b55209e6565';
$user_type = 'admin'; // admin or customer id
$id = '8'; // admin id or customer id
$email = '[email protected]'; // admin email or customer email

// Step 1: Request a new device code
$ch = curl_init('https://example.com/oauth2/device/request');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
// curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
    'client_id' => $client_id
]));

$response = curl_exec($ch);
curl_close($ch);

$device_info = json_decode($response, true);
$device_code = $device_info['data']['device_code'];
$user_code = $device_info['data']['user_code'];

echo "Device Code: $device_code\n";
echo "User Code: $user_code\n";

// Step 2: Authorize the device
$ch = curl_init('https://example.com/oauth2/device/authorize');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
// curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
    'user_code' => $user_code,
    'client_secret' => $client_secret,
    'client_id' => $client_id,
    'user_type' => $user_type,
    'id' => $id,
    'email' => $email
]));

$response = curl_exec($ch);
curl_close($ch);

echo "Device Authorized: $response\n";

// Step 3: Get the access token
$ch = curl_init('https://example.com/oauth2/device/poll');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
// curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
    'device_code' => $device_code,
    'user_type' => $user_type,
    'id' => $id
]));

$response = curl_exec($ch);
curl_close($ch);

$token_info = json_decode($response, true);
$access_token = $token_info['data']['access_token'];
$refresh_token = $token_info['data']['refresh_token'];

echo "Access Token: $access_token\n";
echo "Refresh Token: $refresh_token\n";

// Step 4: Test the token
$ch = curl_init('https://example.com/api/rest/products?limit=10&page=1');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
// curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Accept-Encoding: application/json',
    "Authorization: Bearer $access_token"
]);

$response = curl_exec($ch);
curl_close($ch);

echo "Products: $response\n";
?>

@github-actions github-actions bot added the translations Relates to app/locale label Jul 23, 2024
@sreichel
Copy link
Contributor

sreichel commented Oct 17, 2024

You can run composer run rector:fix to fix that issue.

@sreichel
Copy link
Contributor

Looks good 👍

Some anoying stuff ...

  • please add copyright headers to new files
  • add return types (and type hints) to new methods
  • run n98-magerun.phar dev:ide:phpstorm:meta to update phpstorm meta files, or
ddev magerun dev:ide:phpstorm:meta

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Component: Api2 Relates to Mage_Api2 Template : base Relates to base template translations Relates to app/locale
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants