From b317375a7f63a79bef3bfb999680ddebbba0bb8b Mon Sep 17 00:00:00 2001 From: Louis Chan Date: Thu, 27 Jun 2024 18:46:37 +0800 Subject: [PATCH 1/9] Initial draft on Account Management API --- docs/specs/account-management-api.md | 861 +++++++++++++++++++++++++++ 1 file changed, 861 insertions(+) create mode 100644 docs/specs/account-management-api.md diff --git a/docs/specs/account-management-api.md b/docs/specs/account-management-api.md new file mode 100644 index 0000000000..08134a2fa6 --- /dev/null +++ b/docs/specs/account-management-api.md @@ -0,0 +1,861 @@ +# Account Management API + +Account Management API is an API for account management on behalf of the end user. Account Management API requires a authenticated session, either by cookie or the HTTP header Authorization. + +Account Management API supports the following operations: + +- [Manage identification methods](#manage-identification-methods) + - [List identification methods](#list-identification-methods) + - [Add a new email, with verification](#add-a-new-email-with-verification) + - [Add a new phone number, with verification](#add-a-new-phone-number-with-verification) + - [Add a new username](#add-a-new-username) + - [Add an OAuth provider account, with authorization code flow](#add-an-oauth-provider-account-with-authorization-code-flow) + - [Add a biometric](#add-a-biometric) + - Add a passkey + - [Remove an email](#remove-an-email) + - [Remove a phone number](#remove-a-phone-number) + - [Remove a username](#remove-a-username) + - [Remove an OAuth provider account](#remove-an-oauth-provider-account) + - [Remove a biometric](#remove-a-biometric) + - [Remove a passkey](#remove-a-passkey) + - [Update an email, with verification](#update-an-email-with-verification) + - [Update a phone number, with verification](#update-a-phone-number-with-verification) + - [Update a username](#update-a-username) +- Manage authentication methods + - List authentication methods, and recovery codes. + - Change the primary password + - Change the secondary password + - Remove the secondary password + - Add a new TOTP authenticator + - Remove a TOTP authenticator + - Add a new OOB-OTP authenticator, with verification. + - Remove a OOB-OTP authenticator + - Re-generate recovery codes +- Manage user profile + - Update simple standard attributes + - Update custom attributes + - Add user profile picture + - Replace user profile picture + - Remove user profile picture +- Manage sessions + - List sessions + - Revoke a session + - Terminate all other sessions +- [Auxiliary operations](#auxiliary-operations) + - [Verify OTP](#verify-otp) + - [Resend OTP](#resend-otp) + +## Manage identification methods + +### List identification methods + +`GET /api/v1/account/identification` + +Response + +```json +{ + "result": { + "identification_methods": [ + { + "identification": "email", + "login_id": "user@example.com", + "claims": { + "email": "user@example.com" + }, + "created_at": "2006-01-02T03:04:05Z", + "updated_at": "2006-01-02T03:04:05Z" + }, + { + "identification": "username", + "login_id": "johndoe", + "claims": { + "preferred_username": "johndoe" + }, + "created_at": "2006-01-02T03:04:05Z", + "updated_at": "2006-01-02T03:04:05Z" + }, + { + "identification": "phone", + "login_id": "+85251000001", + "claims": { + "phone_number": "+85251000001" + }, + "created_at": "2006-01-02T03:04:05Z", + "updated_at": "2006-01-02T03:04:05Z" + }, + { + "identification": "oauth", + "provider_type": "google", + "provider_user_id": "USER_ID_AT_GOOGLE", + "alias": "google", + "claims": { + "email": "user@gmail.com" + }, + "created_at": "2006-01-02T03:04:05Z", + "updated_at": "2006-01-02T03:04:05Z" + }, + { + "identification": "passkey", + "credential_id": "hlNmaS6DQjV4voxP8SPJDzDG-j79nWL8r4OTgcPizi0" + "created_at": "2006-01-02T03:04:05Z", + "updated_at": "2006-01-02T03:04:05Z" + }, + { + "identification": "biometric", + "key_id": "KEY_ID", + "display_name": "iPhone 12 mini", + "created_at": "2006-01-02T03:04:05Z", + "updated_at": "2006-01-02T03:04:05Z" + } + ] + } +} +``` + +### Add a new email, with verification + +`POST /api/v1/account/identification` + +Request + +```json +{ + "identification": "email", + "login_id": "user@example.com" +} +``` + +Response + +If verification is not required, then the email is added immediately. + +```json +{ + "result": { + "identification_method": { + "identification": "email", + "login_id": "user@example.com", + "claims": { + "email": "user@example.com" + }, + "created_at": "2006-01-02T03:04:05Z", + "updated_at": "2006-01-02T03:04:05Z" + } + } +} +``` + +If verification is required, you need to perform verification. + +```json +{ + "result": { + "verification": { + "token": "verificationtoken_blahblahblah", + "channel": "email", + "otp_form": "code", + "code_length": 6, + "can_resend_at": "2023-09-21T00:00:00+08:00", + "can_check": false, + "failed_attempt_rate_limit_exceeded": false + } + } +} +``` + +Use [Verify OTP](#verify-otp) and [Resend OTP](#resend-otp) to continue. + +If the email is already taken by another account, then you will receive the following error. + +```json +{ + "error": { + "name": "Invalid", + "reason": "InvariantViolated", + "message": "identity already exists", + "code": 400, + "info": { + "cause": { + "kind": "DuplicatedIdentity" + } + } + } +} +``` + +### Add a new phone number, with verification + +`POST /api/v1/account/identification` + +Request + +```json +{ + "identification": "phone", + "login_id": "+85251000001" +} +``` + +Response + +If verification is not required, then the phone number is added immediately. + +```json +{ + "result": { + "identification_method": { + "identification": "phone", + "login_id": "+85251000001", + "claims": { + "phone_number": "+85251000001" + }, + "created_at": "2006-01-02T03:04:05Z", + "updated_at": "2006-01-02T03:04:05Z" + } + } +} +``` + +If verification is required, you need to perform verification. + +```json +{ + "result": { + "verification": { + "token": "verificationtoken_blahblahblah", + "channel": "sms", + "otp_form": "code", + "code_length": 6, + "can_resend_at": "2023-09-21T00:00:00+08:00", + "can_check": false, + "failed_attempt_rate_limit_exceeded": false + } + } +} +``` + +Use [Verify OTP](#verify-otp) and [Resend OTP](#resend-otp) to continue. + +If the phone number is already taken by another account, then you will receive the following error. + +```json +{ + "error": { + "name": "Invalid", + "reason": "InvariantViolated", + "message": "identity already exists", + "code": 400, + "info": { + "cause": { + "kind": "DuplicatedIdentity" + } + } + } +} +``` + +### Add a new username + +`POST /api/v1/account/identification` + +Request + +```json +{ + "identification": "username", + "login_id": "johndoe" +} +``` + +Response + +```json +{ + "result": { + "identification_method": { + "identification": "username", + "login_id": "johndoe", + "claims": { + "preferred_username": "johndoe" + }, + "created_at": "2006-01-02T03:04:05Z", + "updated_at": "2006-01-02T03:04:05Z" + } + } +} +``` + +If the username is already taken by another account, then you will receive the following error. + +```json +{ + "error": { + "name": "Invalid", + "reason": "InvariantViolated", + "message": "identity already exists", + "code": 400, + "info": { + "cause": { + "kind": "DuplicatedIdentity" + } + } + } +} +``` + +### Add an OAuth provider account, with authorization code flow + +> This endpoint is considered as advanced. It is assumed that you are capable of +> receiving OAuth callback via `redirect_uri`. + +`POST /api/v2/account/identification` + +Request + +```json +{ + "identification": "oauth", + "alias": "google", + "redirect_uri": "https://myapp.authgear.cloud/sso/oauth2/callback/google" +} +``` + +- `redirect_uri`: The settings page of Authgear uses `/sso/oauth2/callback/{alias}` to receive the OAuth callback. You have to specify your own redirect URI to your app or your website. + +Response + +```json +{ + "result": { + "oauth": { + "token": "oauthtoken_blahblahblah", + "authorization_url": "https://www.google.com?client_id=client_id&rredirect_uri=redirect_uri" + } + } +} +``` + +- `oauth.authorization_url`: You MUST redirect the end-user to this URL to continue the authorization code flow. You can add `state` to the URL to help you maintain state and do CSRF protection. + +Finally, the OAuth provider will call your redirect URI with `state` (if you have provided), and other query parameters. You then call the following endpoint. + +`POST /api/v2/account/identification/oauth` + +Request + +```json +{ + "token": "oauthtoken_blahblahblah", + "query": "code=code" +} +``` + +- `token`: `oauth.token` in the previous response. +- `query`: The query of the redirect URI. + +Response + +If successful, then the OAuth provider account is added. + +```json +{ + "result": { + "identification_method": { + "identification": "oauth", + "provider_type": "google", + "provider_user_id": "USER_ID_AT_GOOGLE", + "alias": "google", + "claims": { + "email": "user@gmail.com" + }, + "created_at": "2006-01-02T03:04:05Z", + "updated_at": "2006-01-02T03:04:05Z" + } + } +} +``` + +If the OAuth provider account is already taken by another account, then you will receive the following error. + +```json +{ + "error": { + "name": "Invalid", + "reason": "InvariantViolated", + "message": "identity already exists", + "code": 400, + "info": { + "cause": { + "kind": "DuplicatedIdentity" + } + } + } +} +``` + +### Add a biometric + +Please use the existing SDK method `enableBiometric()` to do so. + +### Remove an email + +`DELETE /api/v1/account/identification` + +Request + +```json +{ + "identification": "email", + "login_id": "user@example.com" +} +``` + +Response + +```json +{ + "result": {} +} +``` + +If it is disallowed to delete an email, you will receive the following error. + +```json +{ + "error": { + "name": "Invalid", + "reason": "InvariantViolated", + "message": "identity modification disabled", + "code": 400, + "info": { + "cause": { + "kind": "IdentityModifyDisabled" + } + } + } +} +``` + +### Remove a phone number + +`DELETE /api/v1/account/identification` + +Request + +```json +{ + "identification": "phone", + "login_id": "+85251000001" +} +``` + +Response + +```json +{ + "result": {} +} +``` + +If it is disallowed to delete a phone number, you will receive the following error. + +```json +{ + "error": { + "name": "Invalid", + "reason": "InvariantViolated", + "message": "identity modification disabled", + "code": 400, + "info": { + "cause": { + "kind": "IdentityModifyDisabled" + } + } + } +} +``` + +### Remove a username + +`DELETE /api/v1/account/identification` + +Request + +```json +{ + "identification": "username", + "login_id": "johndoe" +} +``` + +Response + +```json +{ + "result": {} +} +``` + +If it is disallowed to delete a username, you will receive the following error. + +```json +{ + "error": { + "name": "Invalid", + "reason": "InvariantViolated", + "message": "identity modification disabled", + "code": 400, + "info": { + "cause": { + "kind": "IdentityModifyDisabled" + } + } + } +} +``` + +### Remove an OAuth provider account + +`DELETE /api/v1/account/identification` + +Request + +```json +{ + "identification": "oauth", + "alias": "google", + "provider_user_id": "USER_ID_AT_GOOGLE" +} +``` + +Response + +```json +{ + "result": {} +} +``` + +If it is disallowed to delete an OAuth provider account, you will receive the following error. + +```json +{ + "error": { + "name": "Invalid", + "reason": "InvariantViolated", + "message": "identity modification disabled", + "code": 400, + "info": { + "cause": { + "kind": "IdentityModifyDisabled" + } + } + } +} +``` + +### Remove a biometric + +`DELETE /api/v1/account/identification` + +Request + +```json +{ + "identification": "biometric", + "key_id": "KEY_ID" +} +``` + +Response + +```json +{ + "result": {} +} +``` + +### Remove a passkey + +`DELETE /api/v1/account/identification` + +Request + +```json +{ + "identification": "biometric", + "credential_id": "hlNmaS6DQjV4voxP8SPJDzDG-j79nWL8r4OTgcPizi0" +} +``` + +Response + +```json +{ + "result": {} +} +``` + +### Update an email, with verification + +`PUT /api/v1/account/identification` + +Request + +```json +{ + "identification": "email", + "old_login_id": "user@example.com", + "new_login_id": "user1@example.com" +} +``` + +Response + +If verification is not required, then the email is updated immediately. + +```json +{ + "result": { + "identification_method": { + "identification": "email", + "login_id": "user1@example.com", + "claims": { + "email": "user1@example.com" + }, + "created_at": "2006-01-02T03:04:05Z", + "updated_at": "2006-01-02T03:04:05Z" + } + } +} +``` + +If verification is required, you need to perform verification. + +```json +{ + "result": { + "verification": { + "token": "verificationtoken_blahblahblah", + "channel": "email", + "otp_form": "code", + "code_length": 6, + "can_resend_at": "2023-09-21T00:00:00+08:00", + "can_check": false, + "failed_attempt_rate_limit_exceeded": false + } + } +} +``` + +Use [Verify OTP](#verify-otp) and [Resend OTP](#resend-otp) to continue. + +If the email is already taken by another account, then you will receive the following error. + +```json +{ + "error": { + "name": "Invalid", + "reason": "InvariantViolated", + "message": "identity already exists", + "code": 400, + "info": { + "cause": { + "kind": "DuplicatedIdentity" + } + } + } +} +``` + +### Update a phone number, with verification + +`PUT /api/v1/account/identification` + +Request + +```json +{ + "identification": "phone", + "old_login_id": "+85251000001", + "new_login_id": "+85251000002" +} +``` + +Response + +If verification is not required, then the phone number is updated immediately. + +```json +{ + "result": { + "identification_method": { + "identification": "phone", + "login_id": "+85251000002", + "claims": { + "phone_number": "+85251000002" + }, + "created_at": "2006-01-02T03:04:05Z", + "updated_at": "2006-01-02T03:04:05Z" + } + } +} +``` + +If verification is required, you need to perform verification. + +```json +{ + "result": { + "verification": { + "token": "verificationtoken_blahblahblah", + "channel": "sms", + "otp_form": "code", + "code_length": 6, + "can_resend_at": "2023-09-21T00:00:00+08:00", + "can_check": false, + "failed_attempt_rate_limit_exceeded": false + } + } +} +``` + +Use [Verify OTP](#verify-otp) and [Resend OTP](#resend-otp) to continue. + +If the phone number is already taken by another account, then you will receive the following error. + +```json +{ + "error": { + "name": "Invalid", + "reason": "InvariantViolated", + "message": "identity already exists", + "code": 400, + "info": { + "cause": { + "kind": "DuplicatedIdentity" + } + } + } +} +``` + +### Update a username + +`PUT /api/v1/account/identification` + +Request + +```json +{ + "identification": "username", + "old_login_id": "johndoe", + "new_login_id": "janedoe" +} +``` + +Response + +```json +{ + "result": { + "identification_method": { + "identification": "username", + "login_id": "janedoe", + "claims": { + "preferred_username": "janedoe" + }, + "created_at": "2006-01-02T03:04:05Z", + "updated_at": "2006-01-02T03:04:05Z" + } + } +} +``` + +If the username is already taken by another account, then you will receive the following error. + +```json +{ + "error": { + "name": "Invalid", + "reason": "InvariantViolated", + "message": "identity already exists", + "code": 400, + "info": { + "cause": { + "kind": "DuplicatedIdentity" + } + } + } +} +``` + +## Auxiliary operations + +### Verify OTP + +`POST /api/v1/account/otp/verify` + +Request + +```json +{ + "token": "verificationtoken_blahblahblah", + "code": "123456" +} +``` + +Response + +If successful, then the response is different depending on what operation you are originally performing. + +If the OTP is incorrect, you will receive the following error + +```json +{ + "error": { + "name": "Forbidden", + "reason": "InvalidOTPCode", + "message": "invalid otp code", + "code": 403, + "info": { + "cause": { + "kind": "InvalidCode" + } + } + } +} +``` + +### Resend OTP + +`POST /api/v1/account/otp/resend` + +Request + +```json +{ + "token": "verificationtoken_blahblahblah" +} +``` + +Response + +The `can_resend_at` is updated. + +```json +{ + "result": { + "verification": { + "token": "verificationtoken_blahblahblah", + "channel": "email", + "otp_form": "code", + "code_length": 6, + "can_resend_at": "2023-09-21T00:00:00+08:00", + "can_check": false, + "failed_attempt_rate_limit_exceeded": false + } + } +} +``` From 9b13d4f218f925e357409fccc7647fb80e12f2b9 Mon Sep 17 00:00:00 2001 From: Louis Chan Date: Wed, 3 Jul 2024 18:57:29 +0800 Subject: [PATCH 2/9] Rename xxx methods to xxxs --- docs/specs/account-management-api.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/specs/account-management-api.md b/docs/specs/account-management-api.md index 08134a2fa6..712cda1da8 100644 --- a/docs/specs/account-management-api.md +++ b/docs/specs/account-management-api.md @@ -4,8 +4,8 @@ Account Management API is an API for account management on behalf of the end use Account Management API supports the following operations: -- [Manage identification methods](#manage-identification-methods) - - [List identification methods](#list-identification-methods) +- [Manage identifications](#manage-identifications) + - [List identifications](#list-identifications) - [Add a new email, with verification](#add-a-new-email-with-verification) - [Add a new phone number, with verification](#add-a-new-phone-number-with-verification) - [Add a new username](#add-a-new-username) @@ -21,8 +21,8 @@ Account Management API supports the following operations: - [Update an email, with verification](#update-an-email-with-verification) - [Update a phone number, with verification](#update-a-phone-number-with-verification) - [Update a username](#update-a-username) -- Manage authentication methods - - List authentication methods, and recovery codes. +- Manage authentications + - List authentications, and recovery codes. - Change the primary password - Change the secondary password - Remove the secondary password @@ -45,9 +45,9 @@ Account Management API supports the following operations: - [Verify OTP](#verify-otp) - [Resend OTP](#resend-otp) -## Manage identification methods +## Manage identifications -### List identification methods +### List identifications `GET /api/v1/account/identification` @@ -56,7 +56,7 @@ Response ```json { "result": { - "identification_methods": [ + "identifications": [ { "identification": "email", "login_id": "user@example.com", From 6bba9d297ed4e80c9e1ea72ed3f973d7d0a3697c Mon Sep 17 00:00:00 2001 From: Louis Chan Date: Thu, 11 Jul 2024 16:09:52 +0800 Subject: [PATCH 3/9] Do not specify the APIs that we will not implement at the moment --- docs/specs/account-management-api.md | 757 +-------------------------- 1 file changed, 17 insertions(+), 740 deletions(-) diff --git a/docs/specs/account-management-api.md b/docs/specs/account-management-api.md index 712cda1da8..f69008bbea 100644 --- a/docs/specs/account-management-api.md +++ b/docs/specs/account-management-api.md @@ -5,22 +5,22 @@ Account Management API is an API for account management on behalf of the end use Account Management API supports the following operations: - [Manage identifications](#manage-identifications) - - [List identifications](#list-identifications) - - [Add a new email, with verification](#add-a-new-email-with-verification) - - [Add a new phone number, with verification](#add-a-new-phone-number-with-verification) - - [Add a new username](#add-a-new-username) + - List identifications + - Add a new email, with verification + - Add a new phone number, with verification + - Add a new username - [Add an OAuth provider account, with authorization code flow](#add-an-oauth-provider-account-with-authorization-code-flow) - - [Add a biometric](#add-a-biometric) + - Add a biometric - Add a passkey - - [Remove an email](#remove-an-email) - - [Remove a phone number](#remove-a-phone-number) - - [Remove a username](#remove-a-username) - - [Remove an OAuth provider account](#remove-an-oauth-provider-account) - - [Remove a biometric](#remove-a-biometric) - - [Remove a passkey](#remove-a-passkey) - - [Update an email, with verification](#update-an-email-with-verification) - - [Update a phone number, with verification](#update-a-phone-number-with-verification) - - [Update a username](#update-a-username) + - Remove an email + - Remove a phone number + - Remove a username + - Remove an OAuth provider account + - Remove a biometric + - Remove a passkey + - Update an email, with verification + - Update a phone number, with verification + - Update a username - Manage authentications - List authentications, and recovery codes. - Change the primary password @@ -41,269 +41,12 @@ Account Management API supports the following operations: - List sessions - Revoke a session - Terminate all other sessions -- [Auxiliary operations](#auxiliary-operations) - - [Verify OTP](#verify-otp) - - [Resend OTP](#resend-otp) +- Auxiliary operations + - Verify OTP + - Resend OTP ## Manage identifications -### List identifications - -`GET /api/v1/account/identification` - -Response - -```json -{ - "result": { - "identifications": [ - { - "identification": "email", - "login_id": "user@example.com", - "claims": { - "email": "user@example.com" - }, - "created_at": "2006-01-02T03:04:05Z", - "updated_at": "2006-01-02T03:04:05Z" - }, - { - "identification": "username", - "login_id": "johndoe", - "claims": { - "preferred_username": "johndoe" - }, - "created_at": "2006-01-02T03:04:05Z", - "updated_at": "2006-01-02T03:04:05Z" - }, - { - "identification": "phone", - "login_id": "+85251000001", - "claims": { - "phone_number": "+85251000001" - }, - "created_at": "2006-01-02T03:04:05Z", - "updated_at": "2006-01-02T03:04:05Z" - }, - { - "identification": "oauth", - "provider_type": "google", - "provider_user_id": "USER_ID_AT_GOOGLE", - "alias": "google", - "claims": { - "email": "user@gmail.com" - }, - "created_at": "2006-01-02T03:04:05Z", - "updated_at": "2006-01-02T03:04:05Z" - }, - { - "identification": "passkey", - "credential_id": "hlNmaS6DQjV4voxP8SPJDzDG-j79nWL8r4OTgcPizi0" - "created_at": "2006-01-02T03:04:05Z", - "updated_at": "2006-01-02T03:04:05Z" - }, - { - "identification": "biometric", - "key_id": "KEY_ID", - "display_name": "iPhone 12 mini", - "created_at": "2006-01-02T03:04:05Z", - "updated_at": "2006-01-02T03:04:05Z" - } - ] - } -} -``` - -### Add a new email, with verification - -`POST /api/v1/account/identification` - -Request - -```json -{ - "identification": "email", - "login_id": "user@example.com" -} -``` - -Response - -If verification is not required, then the email is added immediately. - -```json -{ - "result": { - "identification_method": { - "identification": "email", - "login_id": "user@example.com", - "claims": { - "email": "user@example.com" - }, - "created_at": "2006-01-02T03:04:05Z", - "updated_at": "2006-01-02T03:04:05Z" - } - } -} -``` - -If verification is required, you need to perform verification. - -```json -{ - "result": { - "verification": { - "token": "verificationtoken_blahblahblah", - "channel": "email", - "otp_form": "code", - "code_length": 6, - "can_resend_at": "2023-09-21T00:00:00+08:00", - "can_check": false, - "failed_attempt_rate_limit_exceeded": false - } - } -} -``` - -Use [Verify OTP](#verify-otp) and [Resend OTP](#resend-otp) to continue. - -If the email is already taken by another account, then you will receive the following error. - -```json -{ - "error": { - "name": "Invalid", - "reason": "InvariantViolated", - "message": "identity already exists", - "code": 400, - "info": { - "cause": { - "kind": "DuplicatedIdentity" - } - } - } -} -``` - -### Add a new phone number, with verification - -`POST /api/v1/account/identification` - -Request - -```json -{ - "identification": "phone", - "login_id": "+85251000001" -} -``` - -Response - -If verification is not required, then the phone number is added immediately. - -```json -{ - "result": { - "identification_method": { - "identification": "phone", - "login_id": "+85251000001", - "claims": { - "phone_number": "+85251000001" - }, - "created_at": "2006-01-02T03:04:05Z", - "updated_at": "2006-01-02T03:04:05Z" - } - } -} -``` - -If verification is required, you need to perform verification. - -```json -{ - "result": { - "verification": { - "token": "verificationtoken_blahblahblah", - "channel": "sms", - "otp_form": "code", - "code_length": 6, - "can_resend_at": "2023-09-21T00:00:00+08:00", - "can_check": false, - "failed_attempt_rate_limit_exceeded": false - } - } -} -``` - -Use [Verify OTP](#verify-otp) and [Resend OTP](#resend-otp) to continue. - -If the phone number is already taken by another account, then you will receive the following error. - -```json -{ - "error": { - "name": "Invalid", - "reason": "InvariantViolated", - "message": "identity already exists", - "code": 400, - "info": { - "cause": { - "kind": "DuplicatedIdentity" - } - } - } -} -``` - -### Add a new username - -`POST /api/v1/account/identification` - -Request - -```json -{ - "identification": "username", - "login_id": "johndoe" -} -``` - -Response - -```json -{ - "result": { - "identification_method": { - "identification": "username", - "login_id": "johndoe", - "claims": { - "preferred_username": "johndoe" - }, - "created_at": "2006-01-02T03:04:05Z", - "updated_at": "2006-01-02T03:04:05Z" - } - } -} -``` - -If the username is already taken by another account, then you will receive the following error. - -```json -{ - "error": { - "name": "Invalid", - "reason": "InvariantViolated", - "message": "identity already exists", - "code": 400, - "info": { - "cause": { - "kind": "DuplicatedIdentity" - } - } - } -} -``` - ### Add an OAuth provider account, with authorization code flow > This endpoint is considered as advanced. It is assumed that you are capable of @@ -393,469 +136,3 @@ If the OAuth provider account is already taken by another account, then you will } } ``` - -### Add a biometric - -Please use the existing SDK method `enableBiometric()` to do so. - -### Remove an email - -`DELETE /api/v1/account/identification` - -Request - -```json -{ - "identification": "email", - "login_id": "user@example.com" -} -``` - -Response - -```json -{ - "result": {} -} -``` - -If it is disallowed to delete an email, you will receive the following error. - -```json -{ - "error": { - "name": "Invalid", - "reason": "InvariantViolated", - "message": "identity modification disabled", - "code": 400, - "info": { - "cause": { - "kind": "IdentityModifyDisabled" - } - } - } -} -``` - -### Remove a phone number - -`DELETE /api/v1/account/identification` - -Request - -```json -{ - "identification": "phone", - "login_id": "+85251000001" -} -``` - -Response - -```json -{ - "result": {} -} -``` - -If it is disallowed to delete a phone number, you will receive the following error. - -```json -{ - "error": { - "name": "Invalid", - "reason": "InvariantViolated", - "message": "identity modification disabled", - "code": 400, - "info": { - "cause": { - "kind": "IdentityModifyDisabled" - } - } - } -} -``` - -### Remove a username - -`DELETE /api/v1/account/identification` - -Request - -```json -{ - "identification": "username", - "login_id": "johndoe" -} -``` - -Response - -```json -{ - "result": {} -} -``` - -If it is disallowed to delete a username, you will receive the following error. - -```json -{ - "error": { - "name": "Invalid", - "reason": "InvariantViolated", - "message": "identity modification disabled", - "code": 400, - "info": { - "cause": { - "kind": "IdentityModifyDisabled" - } - } - } -} -``` - -### Remove an OAuth provider account - -`DELETE /api/v1/account/identification` - -Request - -```json -{ - "identification": "oauth", - "alias": "google", - "provider_user_id": "USER_ID_AT_GOOGLE" -} -``` - -Response - -```json -{ - "result": {} -} -``` - -If it is disallowed to delete an OAuth provider account, you will receive the following error. - -```json -{ - "error": { - "name": "Invalid", - "reason": "InvariantViolated", - "message": "identity modification disabled", - "code": 400, - "info": { - "cause": { - "kind": "IdentityModifyDisabled" - } - } - } -} -``` - -### Remove a biometric - -`DELETE /api/v1/account/identification` - -Request - -```json -{ - "identification": "biometric", - "key_id": "KEY_ID" -} -``` - -Response - -```json -{ - "result": {} -} -``` - -### Remove a passkey - -`DELETE /api/v1/account/identification` - -Request - -```json -{ - "identification": "biometric", - "credential_id": "hlNmaS6DQjV4voxP8SPJDzDG-j79nWL8r4OTgcPizi0" -} -``` - -Response - -```json -{ - "result": {} -} -``` - -### Update an email, with verification - -`PUT /api/v1/account/identification` - -Request - -```json -{ - "identification": "email", - "old_login_id": "user@example.com", - "new_login_id": "user1@example.com" -} -``` - -Response - -If verification is not required, then the email is updated immediately. - -```json -{ - "result": { - "identification_method": { - "identification": "email", - "login_id": "user1@example.com", - "claims": { - "email": "user1@example.com" - }, - "created_at": "2006-01-02T03:04:05Z", - "updated_at": "2006-01-02T03:04:05Z" - } - } -} -``` - -If verification is required, you need to perform verification. - -```json -{ - "result": { - "verification": { - "token": "verificationtoken_blahblahblah", - "channel": "email", - "otp_form": "code", - "code_length": 6, - "can_resend_at": "2023-09-21T00:00:00+08:00", - "can_check": false, - "failed_attempt_rate_limit_exceeded": false - } - } -} -``` - -Use [Verify OTP](#verify-otp) and [Resend OTP](#resend-otp) to continue. - -If the email is already taken by another account, then you will receive the following error. - -```json -{ - "error": { - "name": "Invalid", - "reason": "InvariantViolated", - "message": "identity already exists", - "code": 400, - "info": { - "cause": { - "kind": "DuplicatedIdentity" - } - } - } -} -``` - -### Update a phone number, with verification - -`PUT /api/v1/account/identification` - -Request - -```json -{ - "identification": "phone", - "old_login_id": "+85251000001", - "new_login_id": "+85251000002" -} -``` - -Response - -If verification is not required, then the phone number is updated immediately. - -```json -{ - "result": { - "identification_method": { - "identification": "phone", - "login_id": "+85251000002", - "claims": { - "phone_number": "+85251000002" - }, - "created_at": "2006-01-02T03:04:05Z", - "updated_at": "2006-01-02T03:04:05Z" - } - } -} -``` - -If verification is required, you need to perform verification. - -```json -{ - "result": { - "verification": { - "token": "verificationtoken_blahblahblah", - "channel": "sms", - "otp_form": "code", - "code_length": 6, - "can_resend_at": "2023-09-21T00:00:00+08:00", - "can_check": false, - "failed_attempt_rate_limit_exceeded": false - } - } -} -``` - -Use [Verify OTP](#verify-otp) and [Resend OTP](#resend-otp) to continue. - -If the phone number is already taken by another account, then you will receive the following error. - -```json -{ - "error": { - "name": "Invalid", - "reason": "InvariantViolated", - "message": "identity already exists", - "code": 400, - "info": { - "cause": { - "kind": "DuplicatedIdentity" - } - } - } -} -``` - -### Update a username - -`PUT /api/v1/account/identification` - -Request - -```json -{ - "identification": "username", - "old_login_id": "johndoe", - "new_login_id": "janedoe" -} -``` - -Response - -```json -{ - "result": { - "identification_method": { - "identification": "username", - "login_id": "janedoe", - "claims": { - "preferred_username": "janedoe" - }, - "created_at": "2006-01-02T03:04:05Z", - "updated_at": "2006-01-02T03:04:05Z" - } - } -} -``` - -If the username is already taken by another account, then you will receive the following error. - -```json -{ - "error": { - "name": "Invalid", - "reason": "InvariantViolated", - "message": "identity already exists", - "code": 400, - "info": { - "cause": { - "kind": "DuplicatedIdentity" - } - } - } -} -``` - -## Auxiliary operations - -### Verify OTP - -`POST /api/v1/account/otp/verify` - -Request - -```json -{ - "token": "verificationtoken_blahblahblah", - "code": "123456" -} -``` - -Response - -If successful, then the response is different depending on what operation you are originally performing. - -If the OTP is incorrect, you will receive the following error - -```json -{ - "error": { - "name": "Forbidden", - "reason": "InvalidOTPCode", - "message": "invalid otp code", - "code": 403, - "info": { - "cause": { - "kind": "InvalidCode" - } - } - } -} -``` - -### Resend OTP - -`POST /api/v1/account/otp/resend` - -Request - -```json -{ - "token": "verificationtoken_blahblahblah" -} -``` - -Response - -The `can_resend_at` is updated. - -```json -{ - "result": { - "verification": { - "token": "verificationtoken_blahblahblah", - "channel": "email", - "otp_form": "code", - "code_length": 6, - "can_resend_at": "2023-09-21T00:00:00+08:00", - "can_check": false, - "failed_attempt_rate_limit_exceeded": false - } - } -} -``` From b58659caaff89c0faeb5eb91ac1c81e91b89739b Mon Sep 17 00:00:00 2001 From: Louis Chan Date: Thu, 11 Jul 2024 19:23:43 +0800 Subject: [PATCH 4/9] Revise the API for adding OAuth provider account --- docs/specs/account-management-api.md | 111 +++++++++++++++++++++------ 1 file changed, 87 insertions(+), 24 deletions(-) diff --git a/docs/specs/account-management-api.md b/docs/specs/account-management-api.md index f69008bbea..848f9c5275 100644 --- a/docs/specs/account-management-api.md +++ b/docs/specs/account-management-api.md @@ -9,7 +9,8 @@ Account Management API supports the following operations: - Add a new email, with verification - Add a new phone number, with verification - Add a new username - - [Add an OAuth provider account, with authorization code flow](#add-an-oauth-provider-account-with-authorization-code-flow) + - [Start adding an OAuth provider account, with authorization code flow](#start-adding-an-oauth-provider-account-with-authorization-code-flow) + - [Finish adding an OAuth provider account, with authorization code flow](#finish-adding-an-oauth-provider-account-with-authorization-code-flow) - Add a biometric - Add a passkey - Remove an email @@ -47,12 +48,12 @@ Account Management API supports the following operations: ## Manage identifications -### Add an OAuth provider account, with authorization code flow +### Start adding an OAuth provider account, with authorization code flow > This endpoint is considered as advanced. It is assumed that you are capable of > receiving OAuth callback via `redirect_uri`. -`POST /api/v2/account/identification` +`POST /api/v1/account/identification` Request @@ -64,6 +65,8 @@ Request } ``` +- `identification`: Required. It must be the value `oauth`. +- `alias`: Required. The alias of the OAuth provider you want the current account to associate with. - `redirect_uri`: The settings page of Authgear uses `/sso/oauth2/callback/{alias}` to receive the OAuth callback. You have to specify your own redirect URI to your app or your website. Response @@ -71,19 +74,52 @@ Response ```json { "result": { - "oauth": { - "token": "oauthtoken_blahblahblah", - "authorization_url": "https://www.google.com?client_id=client_id&rredirect_uri=redirect_uri" - } + "token": "oauthtoken_blahblahblah", + "authorization_url": "https://www.google.com?client_id=client_id&redirect_uri=redirect_uri" } } ``` -- `oauth.authorization_url`: You MUST redirect the end-user to this URL to continue the authorization code flow. You can add `state` to the URL to help you maintain state and do CSRF protection. +- `token`: You store this token. You need to supply it after the end-user returns to your app. +- `authorization_url`: You MUST redirect the end-user to this URL to continue the authorization code flow. You can add `state` to the URL to help you maintain state and do CSRF protection. + +The OAuth provider ultimately will call your redirect URI with query parameters added. You continue the flow with [Finish adding an OAuth provider account, with authorization code flow](#finish-adding-an-oauth-provider-account-with-authorization-code-flow). + +Here is some pseudo code that you should do + +```javascript +const response = fetch("https://myapp.authgear.cloud/api/v1/account/identification", { + method: "POST", + headers: { + "Authorization": `Bearer ${ACCESS_TOKEN}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + "identification": "oauth", + "alias": "google", + "redirect_uri": "com.myapp://host/path", + }), +}); +const responseJSON = await response.json(); +// TODO: Add proper error handling here. +// You cannot assume you always get result. +const token = responseJSON.result.token; +const authorizationURL = new URL(responseJSON.result.authorization_url); +// Generate a random state that is NOT too long. The characters MUST be URL safe. +const state = generateAlphaNumbericRandomStringOfLength(32); +authorizationURL.searchParams.set("state", state); +// Store the state in some persistent storage +window.sessionStorage.setItem("resume", JSON.stringify({ + state, + token, +})); +// Redirect to the OAuth provider. +window.location.href = authorizationURL.toString(); +``` -Finally, the OAuth provider will call your redirect URI with `state` (if you have provided), and other query parameters. You then call the following endpoint. +### Finish adding an OAuth provider account, with authorization code flow -`POST /api/v2/account/identification/oauth` +`POST /api/v1/account/identification/oauth` Request @@ -94,7 +130,7 @@ Request } ``` -- `token`: `oauth.token` in the previous response. +- `token`: The `token` you received in the response of [Start adding an OAuth provider account, with authorization code flow](#start-adding-an-oauth-provider-account-with-authorization-code-flow). - `query`: The query of the redirect URI. Response @@ -103,19 +139,7 @@ If successful, then the OAuth provider account is added. ```json { - "result": { - "identification_method": { - "identification": "oauth", - "provider_type": "google", - "provider_user_id": "USER_ID_AT_GOOGLE", - "alias": "google", - "claims": { - "email": "user@gmail.com" - }, - "created_at": "2006-01-02T03:04:05Z", - "updated_at": "2006-01-02T03:04:05Z" - } - } + "result": {} } ``` @@ -136,3 +160,42 @@ If the OAuth provider account is already taken by another account, then you will } } ``` + +Here is some pseudo code that you should do + +```javascript +// The OAuth provider redirects the user back to us. +const url = new URL(window.location.href); +const state = url.searchParams.get("state"); +if (state == null) { + // Expected state to be present. + return; +} +const resumeStr = window.sessionStorage.getItem("resume"); +if (resumeStr == null) { + // Expected resume to be non-null. + return; +} +// Always remove resume. +window.sessionStorage.removeItem("resume"); + +const resume = JSON.parse(resumeStr); +if (state !== resume.state) { + // Expected resume.state equals to state. + return; +} +const token = resume.token; +const response = fetch("https://myapp.authgear.cloud/api/v1/account/identification/oauth", { + method: "POST", + headers: { + "Authorization": `Bearer ${ACCESS_TOKEN}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + "token": token, + "query": url.search, + }), +}); +const responseJSON = await response.json(); +// TODO: Add proper error handling here. +``` From 5eb29b0761bf2fa33c96c82517fad20e1bfa4905 Mon Sep 17 00:00:00 2001 From: Louis Chan Date: Mon, 15 Jul 2024 20:25:44 +0800 Subject: [PATCH 5/9] Revise the description of redirect_uri --- docs/specs/account-management-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/specs/account-management-api.md b/docs/specs/account-management-api.md index 848f9c5275..f322a13449 100644 --- a/docs/specs/account-management-api.md +++ b/docs/specs/account-management-api.md @@ -67,7 +67,7 @@ Request - `identification`: Required. It must be the value `oauth`. - `alias`: Required. The alias of the OAuth provider you want the current account to associate with. -- `redirect_uri`: The settings page of Authgear uses `/sso/oauth2/callback/{alias}` to receive the OAuth callback. You have to specify your own redirect URI to your app or your website. +- `redirect_uri`: Required. You have to specify your own redirect URI to your app or your website to receive the OAuth callback. Response From 67311d7b8b366cc7b9a1d7bfb894fd516a7b9eda Mon Sep 17 00:00:00 2001 From: Louis Chan Date: Mon, 15 Jul 2024 20:38:02 +0800 Subject: [PATCH 6/9] Add `exclude_state_in_authorization_url` --- docs/specs/account-management-api.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/specs/account-management-api.md b/docs/specs/account-management-api.md index f322a13449..befb70117b 100644 --- a/docs/specs/account-management-api.md +++ b/docs/specs/account-management-api.md @@ -61,13 +61,18 @@ Request { "identification": "oauth", "alias": "google", - "redirect_uri": "https://myapp.authgear.cloud/sso/oauth2/callback/google" + "redirect_uri": "https://myapp.authgear.cloud/sso/oauth2/callback/google", + "exclude_state_in_authorization_url": true } ``` - `identification`: Required. It must be the value `oauth`. - `alias`: Required. The alias of the OAuth provider you want the current account to associate with. - `redirect_uri`: Required. You have to specify your own redirect URI to your app or your website to receive the OAuth callback. +- `exclude_state_in_authorization_url`: Optional. The default is false. + - When it is false, the `authorization_url` has a `state` parameter included, the `token` is bound to this `state` parameter. + - When is is true, the `authorization_url` has no `state` parameter included, the `token` is NOT bound to `state`. + - If you wish to use your own state, you must specify `true` for this field. Response @@ -81,7 +86,7 @@ Response ``` - `token`: You store this token. You need to supply it after the end-user returns to your app. -- `authorization_url`: You MUST redirect the end-user to this URL to continue the authorization code flow. You can add `state` to the URL to help you maintain state and do CSRF protection. +- `authorization_url`: You MUST redirect the end-user to this URL to continue the authorization code flow. If `exclude_state_in_authorization_url` is false, it has `state` parameter included. The OAuth provider ultimately will call your redirect URI with query parameters added. You continue the flow with [Finish adding an OAuth provider account, with authorization code flow](#finish-adding-an-oauth-provider-account-with-authorization-code-flow). @@ -98,6 +103,7 @@ const response = fetch("https://myapp.authgear.cloud/api/v1/account/identificati "identification": "oauth", "alias": "google", "redirect_uri": "com.myapp://host/path", + "exclude_state_in_authorization_url": true, }), }); const responseJSON = await response.json(); From 366d69b24c6710673073bc17699f42af87c27cf5 Mon Sep 17 00:00:00 2001 From: Louis Chan Date: Mon, 15 Jul 2024 20:41:19 +0800 Subject: [PATCH 7/9] Document the possible errors --- docs/specs/account-management-api.md | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/docs/specs/account-management-api.md b/docs/specs/account-management-api.md index befb70117b..cefe18bf48 100644 --- a/docs/specs/account-management-api.md +++ b/docs/specs/account-management-api.md @@ -149,23 +149,13 @@ If successful, then the OAuth provider account is added. } ``` -If the OAuth provider account is already taken by another account, then you will receive the following error. +Error response -```json -{ - "error": { - "name": "Invalid", - "reason": "InvariantViolated", - "message": "identity already exists", - "code": 400, - "info": { - "cause": { - "kind": "DuplicatedIdentity" - } - } - } -} -``` +|Description|Name|Reason|Info| +|---|---|---|---| +|If the OAuth provider account is already taken by another account|Invalid|InvariantViolated|`{"cause": { "kind": "DuplicatedIdentity" } }`| +|If `token` is invalid|Invalid|AccountManagementOAuthTokenInvalid|| +|If `exclude_state_in_authorization_url` is false, and `state` in `query` is not equal to the one bound to `token`|Invalid|AccountManagementOAuthStateNotBoundToToken|| Here is some pseudo code that you should do From f4a60272c22e0851eddf86bdd823fd0f1e0668b2 Mon Sep 17 00:00:00 2001 From: Louis Chan Date: Mon, 15 Jul 2024 20:45:17 +0800 Subject: [PATCH 8/9] Document the error responses in a table --- docs/specs/account-management-api.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/specs/account-management-api.md b/docs/specs/account-management-api.md index cefe18bf48..8c8855baf5 100644 --- a/docs/specs/account-management-api.md +++ b/docs/specs/account-management-api.md @@ -88,6 +88,12 @@ Response - `token`: You store this token. You need to supply it after the end-user returns to your app. - `authorization_url`: You MUST redirect the end-user to this URL to continue the authorization code flow. If `exclude_state_in_authorization_url` is false, it has `state` parameter included. +Error response + +|Description|Name|Reason|Info| +|---|---|---|---| +|If the request is not authenticated|Unauthorized|Unauthorized|| + The OAuth provider ultimately will call your redirect URI with query parameters added. You continue the flow with [Finish adding an OAuth provider account, with authorization code flow](#finish-adding-an-oauth-provider-account-with-authorization-code-flow). Here is some pseudo code that you should do @@ -153,6 +159,7 @@ Error response |Description|Name|Reason|Info| |---|---|---|---| +|If the request is not authenticated|Unauthorized|Unauthorized|| |If the OAuth provider account is already taken by another account|Invalid|InvariantViolated|`{"cause": { "kind": "DuplicatedIdentity" } }`| |If `token` is invalid|Invalid|AccountManagementOAuthTokenInvalid|| |If `exclude_state_in_authorization_url` is false, and `state` in `query` is not equal to the one bound to `token`|Invalid|AccountManagementOAuthStateNotBoundToToken|| From 582980154b3dcde5ed5eda9e6a71be05356ff684 Mon Sep 17 00:00:00 2001 From: Louis Chan Date: Tue, 16 Jul 2024 14:25:41 +0800 Subject: [PATCH 9/9] Add one more error response --- docs/specs/account-management-api.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/specs/account-management-api.md b/docs/specs/account-management-api.md index 8c8855baf5..0bde240f41 100644 --- a/docs/specs/account-management-api.md +++ b/docs/specs/account-management-api.md @@ -163,6 +163,7 @@ Error response |If the OAuth provider account is already taken by another account|Invalid|InvariantViolated|`{"cause": { "kind": "DuplicatedIdentity" } }`| |If `token` is invalid|Invalid|AccountManagementOAuthTokenInvalid|| |If `exclude_state_in_authorization_url` is false, and `state` in `query` is not equal to the one bound to `token`|Invalid|AccountManagementOAuthStateNotBoundToToken|| +|If `token` is not bound to the current user|Invalid|AccountManagementOAuthTokenNotBoundToUser|| Here is some pseudo code that you should do