diff --git a/.github/workflows/build-dereferenced-spec.yml b/.github/workflows/build-dereferenced-spec.yml index 90cf427f7..05a25e103 100644 --- a/.github/workflows/build-dereferenced-spec.yml +++ b/.github/workflows/build-dereferenced-spec.yml @@ -16,6 +16,7 @@ on: - idn/sailpoint-api.v2026.yaml - idn/v2026/** - nerm/** + - idn/oauth/** workflow_dispatch: permissions: @@ -76,6 +77,10 @@ jobs: redocly bundle nerm/v2025/v2025.yaml --ext yaml -o dereferenced/deref-sailpoint-api.nerm.v2025.yaml redocly bundle nerm/v2025/v2025.yaml --ext json -o dereferenced/deref-sailpoint-api.nerm.v2025.json + # oauth + redocly bundle cloud-api-client-common/api-specs/src/main/yaml/sailpoint-oauth.yaml --ext yaml -o dereferenced/deref-sailpoint-api.oauth.yaml + redocly bundle cloud-api-client-common/api-specs/src/main/yaml/sailpoint-oauth.yaml --ext json -o dereferenced/deref-sailpoint-api.oauth.json + # build postman collections openapi2postmanv2 -s dereferenced/deref-sailpoint-api.v3.yaml -o postman/collections/sailpoint-api-v3.json -p -c postman-script/openapi2postman-config.json openapi2postmanv2 -s dereferenced/deref-sailpoint-api.beta.yaml -o postman/collections/sailpoint-api-beta.json -p -c postman-script/openapi2postman-config.json @@ -83,7 +88,8 @@ jobs: openapi2postmanv2 -s dereferenced/deref-sailpoint-api.nerm.v2025.yaml -o postman/collections/sailpoint-api-nerm-v2025.json -p -c postman-script/openapi2postman-config.json openapi2postmanv2 -s dereferenced/deref-sailpoint-api.v2024.yaml -o postman/collections/sailpoint-api-v2024.json -p -c postman-script/openapi2postman-config.json openapi2postmanv2 -s dereferenced/deref-sailpoint-api.v2025.yaml -o postman/collections/sailpoint-api-v2025.json -p -c postman-script/openapi2postman-config.json - openapi2postmanv2 -s dereferenced/deref-sailpoint-api.v2026.yaml -o postman/collections/sailpoint-api-v2026.json -p -c postman-script/openapi2postman-config.json + openapi2postmanv2 -s dereferenced/deref-sailpoint-api.v2026.yaml -o postman/collections/sailpoint-api-v2026.json -p -c postman-script/openapi2postman-config.json + openapi2postmanv2 -s dereferenced/deref-sailpoint-api.oauth.yaml -o postman/collections/sailpoint-api-oauth.json -p -c postman-script/openapi2postman-config.json # modify collections node postman-script/modify-collection.js postman/collections/sailpoint-api-v3.json @@ -93,6 +99,7 @@ jobs: node postman-script/modify-collection.js postman/collections/sailpoint-api-v2026.json node postman-script/modify-collection.js postman/collections/sailpoint-api-nerm.json node postman-script/modify-collection.js postman/collections/sailpoint-api-nerm-v2025.json + node postman-script/modify-collection.js postman/collections/sailpoint-api-oauth.json cd postman-script/update-by-folder-ts @@ -105,6 +112,7 @@ jobs: npm run v2026 npm run nerm npm run nerm-v2025 + npm run oauth - name: Configure Git run: | diff --git a/.gitignore b/.gitignore index 38afeabe5..ef12ec2a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ .env /reports +/cloud-api-client-common +/scripts/temp-collections +/scripts/temp-specs \ No newline at end of file diff --git a/dereferenced/deref-sailpoint-api.oauth.json b/dereferenced/deref-sailpoint-api.oauth.json new file mode 100644 index 000000000..2cc7ee5b9 --- /dev/null +++ b/dereferenced/deref-sailpoint-api.oauth.json @@ -0,0 +1,748 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "SailPoint Identity Security Cloud - OAuth & SAML", + "description": "OAuth 2.0 and SAML endpoints for authentication and token management. Use this spec to generate a Postman collection with correct URLs (tenant + domain, no version path).", + "version": "1.0.0" + }, + "servers": [ + { + "url": "https://{tenant}.api.{domain}.com", + "description": "OAuth API base. Postman will use https://{{tenant}}.api.{{domain}}.com for token, introspect, revoke, and SAML.", + "variables": { + "tenant": { + "default": "sailpoint", + "description": "Tenant name" + }, + "domain": { + "default": "identitynow", + "description": "Domain (e.g. identitynow for identitynow.com)" + } + } + }, + { + "url": "https://{tenant}.login.sailpoint.com", + "description": "OAuth authorize (login). Used for /oauth/authorize.", + "variables": { + "tenant": { + "default": "sailpoint", + "description": "Tenant name" + } + } + } + ], + "tags": [ + { + "name": "OAuth", + "description": "OAuth 2.0 and SAML endpoints for authentication and token management." + } + ], + "paths": { + "/oauth/authorize": { + "servers": [ + { + "url": "https://{tenant}.login.sailpoint.com", + "description": "OAuth authorize (login) server", + "variables": { + "tenant": { + "default": "sailpoint", + "description": "Tenant name" + } + } + } + ], + "get": { + "operationId": "getOauthAuthorize", + "security": [ + {} + ], + "tags": [ + "OAuth" + ], + "summary": "Get OAuth authorization URL", + "description": "Initiates the OAuth 2.0 authorization code flow. The client redirects the user to this endpoint to authenticate with Identity Security Cloud; after successful login, ISC redirects back to the application with an authorization code that can be exchanged for an access token at the token endpoint. PKCE (Proof Key for Code Exchange) is optionally supported: if code_challenge and code_challenge_method are present, sp-token will use them. For public API OAuth clients with authorization_code grant, PKCE is required when that feature is enabled. See [SailPoint Authentication - Authorization Code Flow](https://developer.sailpoint.com/docs/api/v2025/authentication/#request-access-token-with-authorization-code-grant-flow).", + "parameters": [ + { + "in": "query", + "name": "client_id", + "required": true, + "schema": { + "type": "string", + "example": "b61429f5-203d-494c-94c3-04f54e17bc5c" + }, + "description": "The OAuth client ID (e.g. from a personal access token or API client)." + }, + { + "in": "query", + "name": "response_type", + "required": true, + "schema": { + "type": "string", + "enum": [ + "code" + ], + "default": "code", + "example": "code" + }, + "description": "Must be \"code\" for authorization code flow." + }, + { + "in": "query", + "name": "redirect_uri", + "required": true, + "schema": { + "type": "string", + "format": "uri", + "example": "https://myapp.example.com/oauth/redirect" + }, + "description": "The application URL to redirect to after authorization (must match client redirectUris)." + }, + { + "in": "query", + "name": "code_challenge", + "required": false, + "schema": { + "type": "string", + "example": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" + }, + "description": "PKCE code challenge (base64url-encoded). When present, sp-token will validate the code_verifier at the token endpoint." + }, + { + "in": "query", + "name": "code_challenge_method", + "required": false, + "schema": { + "type": "string", + "enum": [ + "S256", + "plain" + ], + "default": "S256", + "example": "S256" + }, + "description": "PKCE code challenge method (S256 recommended). Required when code_challenge is present." + } + ], + "responses": { + "302": { + "description": "Redirect to login or back to redirect_uri with authorization code." + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "429": { + "$ref": "#/components/responses/429" + }, + "500": { + "$ref": "#/components/responses/500" + } + } + } + }, + "/oauth/token": { + "servers": [ + { + "url": "https://{tenant}.api.{domain}.com", + "description": "Use so Postman gets https://{{tenant}}.api.{{domain}}.com/oauth/token (not baseUrl).", + "variables": { + "tenant": { + "default": "sailpoint", + "description": "Tenant name" + }, + "domain": { + "default": "identitynow", + "description": "Domain (e.g. identitynow for identitynow.com)" + } + } + } + ], + "post": { + "operationId": "createOauthToken", + "security": [ + {} + ], + "tags": [ + "OAuth" + ], + "summary": "Create OAuth access token", + "description": "Exchanges credentials or an authorization/refresh code for a JWT access token. Supports client_credentials (PAT or API client), authorization_code (exchange code from /oauth/authorize), and refresh_token grant types. See [SailPoint Authentication](https://developer.sailpoint.com/docs/api/v2025/authentication).", + "requestBody": { + "required": true, + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "required": [ + "grant_type", + "client_id" + ], + "properties": { + "grant_type": { + "type": "string", + "enum": [ + "client_credentials", + "authorization_code", + "refresh_token" + ], + "description": "The OAuth 2.0 grant type." + }, + "client_id": { + "type": "string", + "description": "The OAuth client ID." + }, + "client_secret": { + "type": "string", + "description": "The OAuth client secret (required for client_credentials and refresh_token)." + }, + "code": { + "type": "string", + "description": "Authorization code from /oauth/authorize (required for authorization_code)." + }, + "redirect_uri": { + "type": "string", + "format": "uri", + "description": "Redirect URI used in the authorization request (required for authorization_code)." + }, + "refresh_token": { + "type": "string", + "description": "Refresh token from a previous token response (required for refresh_token)." + }, + "code_verifier": { + "type": "string", + "description": "PKCE code verifier (required when authorization request included code_challenge). Raw value that was used to generate the code_challenge." + }, + "scope": { + "type": "string", + "description": "Optional scope (e.g. sp:scope:all)." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Token response with access_token (JWT), token_type, expires_in, and optionally refresh_token.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "access_token": { + "type": "string", + "description": "JWT access token for API authorization." + }, + "token_type": { + "type": "string", + "example": "bearer" + }, + "expires_in": { + "type": "integer", + "description": "Lifetime of the access token in seconds." + }, + "refresh_token": { + "type": "string", + "description": "Present when using authorization_code with offline access." + }, + "scope": { + "type": "string" + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "429": { + "$ref": "#/components/responses/429" + }, + "500": { + "$ref": "#/components/responses/500" + } + } + } + }, + "/oauth/introspect": { + "servers": [ + { + "url": "https://{tenant}.api.{domain}.com", + "description": "Use so Postman gets https://{{tenant}}.api.{{domain}}.com/oauth/introspect (not baseUrl).", + "variables": { + "tenant": { + "default": "sailpoint", + "description": "Tenant name" + }, + "domain": { + "default": "identitynow", + "description": "Domain (e.g. identitynow for identitynow.com)" + } + } + } + ], + "post": { + "operationId": "testOauthIntrospect", + "security": [ + {} + ], + "tags": [ + "OAuth" + ], + "summary": "Introspect OAuth token (RFC 7662)", + "description": "Standard OAuth 2.0 token introspection (RFC 7662). Validates an access or refresh token and returns its claims and metadata. Send the token in the request body as the \"token\" parameter.", + "requestBody": { + "required": true, + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "string", + "description": "The access_token or refresh_token to introspect (e.g. from the token endpoint response). In Postman, use {{accessToken}} or {{refreshToken}} if set from a previous token request." + }, + "token_type_hint": { + "type": "string", + "enum": [ + "access_token", + "refresh_token" + ], + "description": "Optional hint indicating the type of token being passed (RFC 7662)." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Token introspection result; when active is true, response includes tenant, identity, and authorization details.", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Token introspection result (RFC 7662); active plus claims when token is valid.", + "properties": { + "active": { + "type": "boolean", + "description": "Whether the token is active/valid (RFC 7662)." + }, + "tenant_id": { + "type": "string", + "format": "uuid", + "description": "Tenant UUID (when active)." + }, + "sub": { + "type": "string", + "description": "Subject identifier (e.g. user or client id)." + }, + "pod": { + "type": "string", + "description": "Pod identifier (e.g. stg03-useast1)." + }, + "org": { + "type": "string", + "description": "Organization/tenant name." + }, + "identity_id": { + "type": "string", + "description": "Identity UUID in Identity Security Cloud." + }, + "user_name": { + "type": "string", + "description": "Username of the authenticated user." + }, + "iss": { + "type": "string", + "format": "uri", + "description": "Token issuer (e.g. https://www.sailpoint.com)." + }, + "strong_auth": { + "type": "boolean", + "description": "Whether strong authentication was used." + }, + "authorities": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Granted authorities and scope-based rights." + }, + "client_id": { + "type": "string", + "description": "OAuth client ID used to obtain the token." + }, + "encoded_scope": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Encoded scope representation." + }, + "strong_auth_supported": { + "type": "boolean", + "description": "Whether strong auth is supported for this context." + }, + "scope": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of granted scopes." + }, + "exp": { + "type": "integer", + "description": "Token expiration time (Unix timestamp)." + }, + "jti": { + "type": "string", + "format": "uuid", + "description": "JWT ID (unique token identifier)." + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "429": { + "$ref": "#/components/responses/429" + }, + "500": { + "$ref": "#/components/responses/500" + } + } + } + }, + "/oauth/revoke": { + "servers": [ + { + "url": "https://{tenant}.api.{domain}.com", + "description": "Use so Postman gets https://{{tenant}}.api.{{domain}}.com/oauth/revoke (not baseUrl).", + "variables": { + "tenant": { + "default": "sailpoint", + "description": "Tenant name" + }, + "domain": { + "default": "identitynow", + "description": "Domain (e.g. identitynow for identitynow.com)" + } + } + } + ], + "post": { + "operationId": "updateOauthTokenRevoke", + "security": [ + {} + ], + "tags": [ + "OAuth" + ], + "summary": "Revoke OAuth access or refresh token", + "description": "Revokes an OAuth 2.0 access token or refresh token. Present the token in the request body; the backing implementation places the token's JWT ID (jti) in a cache so that the gateway will no longer accept the access token for API calls, or so that the refresh token cannot be used to obtain new access tokens.\n**Availability:** This endpoint is coming soon and may not yet be exposed via the API Gateway.", + "requestBody": { + "required": true, + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "string", + "description": "The access_token or refresh_token to revoke." + }, + "token_type_hint": { + "type": "string", + "enum": [ + "access_token", + "refresh_token" + ], + "description": "Optional hint indicating the type of token being revoked (RFC 7009)." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Token revoked successfully." + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "429": { + "$ref": "#/components/responses/429" + }, + "500": { + "$ref": "#/components/responses/500" + } + } + } + }, + "/saml/metadata/alias/{tenant}-sp": { + "servers": [ + { + "url": "https://{tenant}.api.{domain}.com", + "description": "Use this URL so Postman generates https://{{tenant}}.api.{{domain}}.com/saml/metadata/alias/{{tenant}}-sp", + "variables": { + "tenant": { + "default": "sailpoint", + "description": "Tenant name" + }, + "domain": { + "default": "identitynow", + "description": "Domain (e.g. identitynow for identitynow.com)" + } + } + } + ], + "get": { + "operationId": "getSamlMetadataAlias", + "security": [ + {} + ], + "tags": [ + "OAuth" + ], + "summary": "Get SAML metadata by alias", + "description": "Returns SAML 2.0 metadata for the Identity Security Cloud service provider. The path uses the tenant in the form {tenant}-sp so the URL is https://{{tenant}}.api.{{domain}}.com/saml/metadata/alias/{{tenant}}-sp.", + "parameters": [ + { + "in": "path", + "name": "tenant", + "required": true, + "schema": { + "type": "string", + "example": "sailpoint" + }, + "description": "Tenant name; the path suffix will be -sp (e.g. sailpoint-sp)." + } + ], + "responses": { + "200": { + "description": "SAML 2.0 metadata XML for the service provider.", + "content": { + "application/xml": { + "schema": { + "type": "string", + "description": "SAML metadata document." + } + } + } + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "429": { + "$ref": "#/components/responses/429" + }, + "500": { + "$ref": "#/components/responses/500" + } + } + } + } + }, + "components": { + "schemas": { + "LocaleOrigin": { + "type": "string", + "enum": [ + "DEFAULT", + "REQUEST", + null + ], + "description": "An indicator of how the locale was selected. *DEFAULT* means the locale is the system default. *REQUEST* means the locale was selected from the request context (i.e., best match based on the *Accept-Language* header). Additional values may be added in the future without notice.", + "example": "DEFAULT", + "nullable": true + }, + "ErrorMessageDto": { + "type": "object", + "title": "Error Message Dto", + "properties": { + "locale": { + "type": "string", + "description": "The locale for the message text, a BCP 47 language tag.", + "example": "en-US", + "nullable": true + }, + "localeOrigin": { + "$ref": "#/components/schemas/LocaleOrigin" + }, + "text": { + "type": "string", + "description": "Actual text of the error message in the indicated locale.", + "example": "The request was syntactically correct but its content is semantically invalid." + } + } + }, + "ErrorResponseDto": { + "type": "object", + "title": "Error Response Dto", + "properties": { + "detailCode": { + "type": "string", + "description": "Fine-grained error code providing more detail of the error.", + "example": "400.1 Bad Request Content" + }, + "trackingId": { + "type": "string", + "description": "Unique tracking id for the error.", + "example": "e7eab60924f64aa284175b9fa3309599" + }, + "messages": { + "type": "array", + "description": "Generic localized reason for error", + "items": { + "$ref": "#/components/schemas/ErrorMessageDto" + } + }, + "causes": { + "type": "array", + "description": "Plain-text descriptive reasons to provide additional detail to the text provided in the messages field", + "items": { + "$ref": "#/components/schemas/ErrorMessageDto" + } + } + } + } + }, + "responses": { + "400": { + "description": "Client Error - Returned if the request body is invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Unauthorized - Returned if there is no authorization header, or if the JWT token is expired.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "description": "A message describing the error", + "example": "JWT validation failed: JWT is expired" + } + } + } + } + } + }, + "403": { + "description": "Forbidden - Returned if the user you are running as, doesn't have access to this end-point.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + }, + "examples": { + "403": { + "summary": "An example of a 403 response object", + "value": { + "detailCode": "403 Forbidden", + "trackingId": "b21b1f7ce4da4d639f2c62a57171b427", + "messages": [ + { + "locale": "en-US", + "localeOrigin": "DEFAULT", + "text": "The server understood the request but refuses to authorize it." + } + ] + } + } + } + } + } + }, + "429": { + "description": "Too Many Requests - Returned in response to too many requests in a given period of time - rate limited. The Retry-After header in the response includes how long to wait before trying again.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "description": "A message describing the error", + "example": " Rate Limit Exceeded " + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error - Returned if there is an unexpected error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + }, + "examples": { + "500": { + "summary": "An example of a 500 response object", + "value": { + "detailCode": "500.0 Internal Fault", + "trackingId": "b21b1f7ce4da4d639f2c62a57171b427", + "messages": [ + { + "locale": "en-US", + "localeOrigin": "DEFAULT", + "text": "An internal fault occurred." + } + ] + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/dereferenced/deref-sailpoint-api.oauth.yaml b/dereferenced/deref-sailpoint-api.oauth.yaml new file mode 100644 index 000000000..d9abd194a --- /dev/null +++ b/dereferenced/deref-sailpoint-api.oauth.yaml @@ -0,0 +1,505 @@ +openapi: 3.0.1 +info: + title: SailPoint Identity Security Cloud - OAuth & SAML + description: OAuth 2.0 and SAML endpoints for authentication and token management. Use this spec to generate a Postman collection with correct URLs (tenant + domain, no version path). + version: 1.0.0 +servers: + - url: https://{tenant}.api.{domain}.com + description: OAuth API base. Postman will use https://{{tenant}}.api.{{domain}}.com for token, introspect, revoke, and SAML. + variables: + tenant: + default: sailpoint + description: Tenant name + domain: + default: identitynow + description: Domain (e.g. identitynow for identitynow.com) + - url: https://{tenant}.login.sailpoint.com + description: OAuth authorize (login). Used for /oauth/authorize. + variables: + tenant: + default: sailpoint + description: Tenant name +tags: + - name: OAuth + description: OAuth 2.0 and SAML endpoints for authentication and token management. +paths: + /oauth/authorize: + servers: + - url: https://{tenant}.login.sailpoint.com + description: OAuth authorize (login) server + variables: + tenant: + default: sailpoint + description: Tenant name + get: + operationId: getOauthAuthorize + security: + - {} + tags: + - OAuth + summary: Get OAuth authorization URL + description: 'Initiates the OAuth 2.0 authorization code flow. The client redirects the user to this endpoint to authenticate with Identity Security Cloud; after successful login, ISC redirects back to the application with an authorization code that can be exchanged for an access token at the token endpoint. PKCE (Proof Key for Code Exchange) is optionally supported: if code_challenge and code_challenge_method are present, sp-token will use them. For public API OAuth clients with authorization_code grant, PKCE is required when that feature is enabled. See [SailPoint Authentication - Authorization Code Flow](https://developer.sailpoint.com/docs/api/v2025/authentication/#request-access-token-with-authorization-code-grant-flow).' + parameters: + - in: query + name: client_id + required: true + schema: + type: string + example: b61429f5-203d-494c-94c3-04f54e17bc5c + description: The OAuth client ID (e.g. from a personal access token or API client). + - in: query + name: response_type + required: true + schema: + type: string + enum: + - code + default: code + example: code + description: Must be "code" for authorization code flow. + - in: query + name: redirect_uri + required: true + schema: + type: string + format: uri + example: https://myapp.example.com/oauth/redirect + description: The application URL to redirect to after authorization (must match client redirectUris). + - in: query + name: code_challenge + required: false + schema: + type: string + example: E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM + description: PKCE code challenge (base64url-encoded). When present, sp-token will validate the code_verifier at the token endpoint. + - in: query + name: code_challenge_method + required: false + schema: + type: string + enum: + - S256 + - plain + default: S256 + example: S256 + description: PKCE code challenge method (S256 recommended). Required when code_challenge is present. + responses: + '302': + description: Redirect to login or back to redirect_uri with authorization code. + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '429': + $ref: '#/components/responses/429' + '500': + $ref: '#/components/responses/500' + /oauth/token: + servers: + - url: https://{tenant}.api.{domain}.com + description: Use so Postman gets https://{{tenant}}.api.{{domain}}.com/oauth/token (not baseUrl). + variables: + tenant: + default: sailpoint + description: Tenant name + domain: + default: identitynow + description: Domain (e.g. identitynow for identitynow.com) + post: + operationId: createOauthToken + security: + - {} + tags: + - OAuth + summary: Create OAuth access token + description: Exchanges credentials or an authorization/refresh code for a JWT access token. Supports client_credentials (PAT or API client), authorization_code (exchange code from /oauth/authorize), and refresh_token grant types. See [SailPoint Authentication](https://developer.sailpoint.com/docs/api/v2025/authentication). + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - grant_type + - client_id + properties: + grant_type: + type: string + enum: + - client_credentials + - authorization_code + - refresh_token + description: The OAuth 2.0 grant type. + client_id: + type: string + description: The OAuth client ID. + client_secret: + type: string + description: The OAuth client secret (required for client_credentials and refresh_token). + code: + type: string + description: Authorization code from /oauth/authorize (required for authorization_code). + redirect_uri: + type: string + format: uri + description: Redirect URI used in the authorization request (required for authorization_code). + refresh_token: + type: string + description: Refresh token from a previous token response (required for refresh_token). + code_verifier: + type: string + description: PKCE code verifier (required when authorization request included code_challenge). Raw value that was used to generate the code_challenge. + scope: + type: string + description: Optional scope (e.g. sp:scope:all). + responses: + '200': + description: Token response with access_token (JWT), token_type, expires_in, and optionally refresh_token. + content: + application/json: + schema: + type: object + properties: + access_token: + type: string + description: JWT access token for API authorization. + token_type: + type: string + example: bearer + expires_in: + type: integer + description: Lifetime of the access token in seconds. + refresh_token: + type: string + description: Present when using authorization_code with offline access. + scope: + type: string + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '429': + $ref: '#/components/responses/429' + '500': + $ref: '#/components/responses/500' + /oauth/introspect: + servers: + - url: https://{tenant}.api.{domain}.com + description: Use so Postman gets https://{{tenant}}.api.{{domain}}.com/oauth/introspect (not baseUrl). + variables: + tenant: + default: sailpoint + description: Tenant name + domain: + default: identitynow + description: Domain (e.g. identitynow for identitynow.com) + post: + operationId: testOauthIntrospect + security: + - {} + tags: + - OAuth + summary: Introspect OAuth token (RFC 7662) + description: Standard OAuth 2.0 token introspection (RFC 7662). Validates an access or refresh token and returns its claims and metadata. Send the token in the request body as the "token" parameter. + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - token + properties: + token: + type: string + description: The access_token or refresh_token to introspect (e.g. from the token endpoint response). In Postman, use {{accessToken}} or {{refreshToken}} if set from a previous token request. + token_type_hint: + type: string + enum: + - access_token + - refresh_token + description: Optional hint indicating the type of token being passed (RFC 7662). + responses: + '200': + description: Token introspection result; when active is true, response includes tenant, identity, and authorization details. + content: + application/json: + schema: + type: object + description: Token introspection result (RFC 7662); active plus claims when token is valid. + properties: + active: + type: boolean + description: Whether the token is active/valid (RFC 7662). + tenant_id: + type: string + format: uuid + description: Tenant UUID (when active). + sub: + type: string + description: Subject identifier (e.g. user or client id). + pod: + type: string + description: Pod identifier (e.g. stg03-useast1). + org: + type: string + description: Organization/tenant name. + identity_id: + type: string + description: Identity UUID in Identity Security Cloud. + user_name: + type: string + description: Username of the authenticated user. + iss: + type: string + format: uri + description: Token issuer (e.g. https://www.sailpoint.com). + strong_auth: + type: boolean + description: Whether strong authentication was used. + authorities: + type: array + items: + type: string + description: Granted authorities and scope-based rights. + client_id: + type: string + description: OAuth client ID used to obtain the token. + encoded_scope: + type: array + items: + type: string + description: Encoded scope representation. + strong_auth_supported: + type: boolean + description: Whether strong auth is supported for this context. + scope: + type: array + items: + type: string + description: List of granted scopes. + exp: + type: integer + description: Token expiration time (Unix timestamp). + jti: + type: string + format: uuid + description: JWT ID (unique token identifier). + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '429': + $ref: '#/components/responses/429' + '500': + $ref: '#/components/responses/500' + /oauth/revoke: + servers: + - url: https://{tenant}.api.{domain}.com + description: Use so Postman gets https://{{tenant}}.api.{{domain}}.com/oauth/revoke (not baseUrl). + variables: + tenant: + default: sailpoint + description: Tenant name + domain: + default: identitynow + description: Domain (e.g. identitynow for identitynow.com) + post: + operationId: updateOauthTokenRevoke + security: + - {} + tags: + - OAuth + summary: Revoke OAuth access or refresh token + description: |- + Revokes an OAuth 2.0 access token or refresh token. Present the token in the request body; the backing implementation places the token's JWT ID (jti) in a cache so that the gateway will no longer accept the access token for API calls, or so that the refresh token cannot be used to obtain new access tokens. + **Availability:** This endpoint is coming soon and may not yet be exposed via the API Gateway. + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - token + properties: + token: + type: string + description: The access_token or refresh_token to revoke. + token_type_hint: + type: string + enum: + - access_token + - refresh_token + description: Optional hint indicating the type of token being revoked (RFC 7009). + responses: + '200': + description: Token revoked successfully. + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '429': + $ref: '#/components/responses/429' + '500': + $ref: '#/components/responses/500' + /saml/metadata/alias/{tenant}-sp: + servers: + - url: https://{tenant}.api.{domain}.com + description: Use this URL so Postman generates https://{{tenant}}.api.{{domain}}.com/saml/metadata/alias/{{tenant}}-sp + variables: + tenant: + default: sailpoint + description: Tenant name + domain: + default: identitynow + description: Domain (e.g. identitynow for identitynow.com) + get: + operationId: getSamlMetadataAlias + security: + - {} + tags: + - OAuth + summary: Get SAML metadata by alias + description: Returns SAML 2.0 metadata for the Identity Security Cloud service provider. The path uses the tenant in the form {tenant}-sp so the URL is https://{{tenant}}.api.{{domain}}.com/saml/metadata/alias/{{tenant}}-sp. + parameters: + - in: path + name: tenant + required: true + schema: + type: string + example: sailpoint + description: Tenant name; the path suffix will be -sp (e.g. sailpoint-sp). + responses: + '200': + description: SAML 2.0 metadata XML for the service provider. + content: + application/xml: + schema: + type: string + description: SAML metadata document. + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '429': + $ref: '#/components/responses/429' + '500': + $ref: '#/components/responses/500' +components: + schemas: + LocaleOrigin: + type: string + enum: + - DEFAULT + - REQUEST + - null + description: An indicator of how the locale was selected. *DEFAULT* means the locale is the system default. *REQUEST* means the locale was selected from the request context (i.e., best match based on the *Accept-Language* header). Additional values may be added in the future without notice. + example: DEFAULT + nullable: true + ErrorMessageDto: + type: object + title: Error Message Dto + properties: + locale: + type: string + description: The locale for the message text, a BCP 47 language tag. + example: en-US + nullable: true + localeOrigin: + $ref: '#/components/schemas/LocaleOrigin' + text: + type: string + description: Actual text of the error message in the indicated locale. + example: The request was syntactically correct but its content is semantically invalid. + ErrorResponseDto: + type: object + title: Error Response Dto + properties: + detailCode: + type: string + description: Fine-grained error code providing more detail of the error. + example: 400.1 Bad Request Content + trackingId: + type: string + description: Unique tracking id for the error. + example: e7eab60924f64aa284175b9fa3309599 + messages: + type: array + description: Generic localized reason for error + items: + $ref: '#/components/schemas/ErrorMessageDto' + causes: + type: array + description: Plain-text descriptive reasons to provide additional detail to the text provided in the messages field + items: + $ref: '#/components/schemas/ErrorMessageDto' + responses: + '400': + description: Client Error - Returned if the request body is invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + '401': + description: Unauthorized - Returned if there is no authorization header, or if the JWT token is expired. + content: + application/json: + schema: + type: object + properties: + error: + description: A message describing the error + example: 'JWT validation failed: JWT is expired' + '403': + description: Forbidden - Returned if the user you are running as, doesn't have access to this end-point. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + examples: + '403': + summary: An example of a 403 response object + value: + detailCode: 403 Forbidden + trackingId: b21b1f7ce4da4d639f2c62a57171b427 + messages: + - locale: en-US + localeOrigin: DEFAULT + text: The server understood the request but refuses to authorize it. + '429': + description: Too Many Requests - Returned in response to too many requests in a given period of time - rate limited. The Retry-After header in the response includes how long to wait before trying again. + content: + application/json: + schema: + type: object + properties: + message: + description: A message describing the error + example: ' Rate Limit Exceeded ' + '500': + description: Internal Server Error - Returned if there is an unexpected error. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + examples: + '500': + summary: An example of a 500 response object + value: + detailCode: 500.0 Internal Fault + trackingId: b21b1f7ce4da4d639f2c62a57171b427 + messages: + - locale: en-US + localeOrigin: DEFAULT + text: An internal fault occurred. diff --git a/postman-script/modify-collection.js b/postman-script/modify-collection.js index bb4ebcd7a..a7a9aaef9 100644 --- a/postman-script/modify-collection.js +++ b/postman-script/modify-collection.js @@ -57,6 +57,8 @@ fs.readFile(args[2], 'utf8', (err, data) => { jsonObject.variable = JSON.parse(fs.readFileSync('postman-script/variable-v2025.json', 'utf8')); } else if (args[2].includes("v2026")) { jsonObject.variable = JSON.parse(fs.readFileSync('postman-script/variable-v2026.json', 'utf8')); + } else if (args[2].includes("oauth")) { + jsonObject.variable = JSON.parse(fs.readFileSync('postman-script/variable-auth.json', 'utf8')); } diff --git a/postman-script/update-by-folder-ts/package.json b/postman-script/update-by-folder-ts/package.json index 20be79cea..8fab175c7 100644 --- a/postman-script/update-by-folder-ts/package.json +++ b/postman-script/update-by-folder-ts/package.json @@ -12,7 +12,8 @@ "v2026": "tsc && node dist/index.js v2026", "beta": "tsc && node dist/index.js beta", "nerm": "tsc && node dist/index.js nerm", - "nerm-v2025": "tsc && node dist/index.js nerm-v2025" + "nerm-v2025": "tsc && node dist/index.js nerm-v2025", + "oauth": "tsc && node dist/index.js oauth" }, "keywords": [], "author": "", diff --git a/postman-script/variable-auth.json b/postman-script/variable-auth.json new file mode 100644 index 000000000..a3e5d3893 --- /dev/null +++ b/postman-script/variable-auth.json @@ -0,0 +1,12 @@ +[ + { + "key": "domain", + "value": "identitynow", + "type": "string" + }, + { + "key": "baseUrl", + "value": "https://{{tenant}}.api.{{domain}}.com", + "type": "string" + } +] \ No newline at end of file diff --git a/postman/collections/sailpoint-api-oauth.json b/postman/collections/sailpoint-api-oauth.json new file mode 100644 index 000000000..12b5735fe --- /dev/null +++ b/postman/collections/sailpoint-api-oauth.json @@ -0,0 +1,758 @@ +{ + "item": [ + { + "name": "OAuth", + "description": "OAuth 2.0 and SAML endpoints for authentication and token management.", + "item": [ + { + "id": "6e86c0e7-7ff1-408f-a3fa-aedbcd520d72", + "name": "Get OAuth authorization URL", + "request": { + "name": "Get OAuth authorization URL", + "description": { + "content": "Initiates the OAuth 2.0 authorization code flow. The client redirects the user to this endpoint to authenticate with Identity Security Cloud; after successful login, ISC redirects back to the application with an authorization code that can be exchanged for an access token at the token endpoint. PKCE (Proof Key for Code Exchange) is optionally supported: if code_challenge and code_challenge_method are present, sp-token will use them. For public API OAuth clients with authorization_code grant, PKCE is required when that feature is enabled. See [SailPoint Authentication - Authorization Code Flow](https://developer.sailpoint.com/docs/api/v2025/authentication/#request-access-token-with-authorization-code-grant-flow).", + "type": "text/plain" + }, + "url": { + "path": [ + "oauth", + "authorize" + ], + "host": [ + "{{baseUrl}}" + ], + "query": [ + { + "disabled": true, + "description": { + "content": "(Required) The OAuth client ID (e.g. from a personal access token or API client).", + "type": "text/plain" + }, + "key": "client_id", + "value": "b61429f5-203d-494c-94c3-04f54e17bc5c" + }, + { + "disabled": true, + "description": { + "content": "(Required) Must be \"code\" for authorization code flow.", + "type": "text/plain" + }, + "key": "response_type", + "value": "code" + }, + { + "disabled": true, + "description": { + "content": "(Required) The application URL to redirect to after authorization (must match client redirectUris).", + "type": "text/plain" + }, + "key": "redirect_uri", + "value": "https://myapp.example.com/oauth/redirect" + }, + { + "disabled": true, + "description": { + "content": "PKCE code challenge (base64url-encoded). When present, sp-token will validate the code_verifier at the token endpoint.", + "type": "text/plain" + }, + "key": "code_challenge", + "value": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" + }, + { + "disabled": true, + "description": { + "content": "PKCE code challenge method (S256 recommended). Required when code_challenge is present.", + "type": "text/plain" + }, + "key": "code_challenge_method", + "value": "S256" + } + ], + "variable": [] + }, + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "method": "GET", + "body": {} + }, + "response": [], + "event": [], + "protocolProfileBehavior": { + "disableBodyPruning": true + } + }, + { + "id": "3fa0cd46-7508-4604-bdc1-53f2223d0a75", + "name": "Create OAuth access token", + "request": { + "name": "Create OAuth access token", + "description": { + "content": "Exchanges credentials or an authorization/refresh code for a JWT access token. Supports client_credentials (PAT or API client), authorization_code (exchange code from /oauth/authorize), and refresh_token grant types. See [SailPoint Authentication](https://developer.sailpoint.com/docs/api/v2025/authentication).", + "type": "text/plain" + }, + "url": { + "path": [ + "oauth", + "token" + ], + "host": [ + "{{baseUrl}}" + ], + "query": [], + "variable": [] + }, + "header": [ + { + "key": "Content-Type", + "value": "application/x-www-form-urlencoded" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "method": "POST", + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "disabled": true, + "description": { + "content": "(Required) The OAuth 2.0 grant type.", + "type": "text/plain" + }, + "key": "grant_type", + "value": "client_credentials" + }, + { + "disabled": true, + "description": { + "content": "(Required) The OAuth client ID.", + "type": "text/plain" + }, + "key": "client_id", + "value": "magna consectetur" + }, + { + "disabled": true, + "description": { + "content": "The OAuth client secret (required for client_credentials and refresh_token).", + "type": "text/plain" + }, + "key": "client_secret", + "value": "eu" + }, + { + "disabled": true, + "description": { + "content": "Authorization code from /oauth/authorize (required for authorization_code).", + "type": "text/plain" + }, + "key": "code", + "value": "enim in" + }, + { + "disabled": true, + "description": { + "content": "Redirect URI used in the authorization request (required for authorization_code).", + "type": "text/plain" + }, + "key": "redirect_uri", + "value": "https://YTVhaKGcQyTlZogiMGzfJDWsdIwcL.zjgouSOCyZdsFiinBGN1Clo-46ZUop5QUBz" + }, + { + "disabled": true, + "description": { + "content": "Refresh token from a previous token response (required for refresh_token).", + "type": "text/plain" + }, + "key": "refresh_token", + "value": "consectetur tempor c" + }, + { + "disabled": true, + "description": { + "content": "PKCE code verifier (required when authorization request included code_challenge). Raw value that was used to generate the code_challenge.", + "type": "text/plain" + }, + "key": "code_verifier", + "value": "Duis" + }, + { + "disabled": true, + "description": { + "content": "Optional scope (e.g. sp:scope:all).", + "type": "text/plain" + }, + "key": "scope", + "value": "esse" + } + ] + } + }, + "response": [ + { + "id": "1e2586ba-0d68-4efd-95cf-eb7abcd7e97b", + "name": "Token response with access_token (JWT), token_type, expires_in, and optionally refresh_token.", + "originalRequest": { + "url": { + "path": [ + "oauth", + "token" + ], + "host": [ + "{{baseUrl}}" + ], + "query": [], + "variable": [] + }, + "header": [ + { + "key": "Content-Type", + "value": "application/x-www-form-urlencoded" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "method": "POST", + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "disabled": true, + "description": { + "content": "(Required) The OAuth 2.0 grant type.", + "type": "text/plain" + }, + "key": "grant_type", + "value": "client_credentials" + }, + { + "disabled": true, + "description": { + "content": "(Required) The OAuth client ID.", + "type": "text/plain" + }, + "key": "client_id", + "value": "magna consectetur" + }, + { + "disabled": true, + "description": { + "content": "The OAuth client secret (required for client_credentials and refresh_token).", + "type": "text/plain" + }, + "key": "client_secret", + "value": "eu" + }, + { + "disabled": true, + "description": { + "content": "Authorization code from /oauth/authorize (required for authorization_code).", + "type": "text/plain" + }, + "key": "code", + "value": "enim in" + }, + { + "disabled": true, + "description": { + "content": "Redirect URI used in the authorization request (required for authorization_code).", + "type": "text/plain" + }, + "key": "redirect_uri", + "value": "https://YTVhaKGcQyTlZogiMGzfJDWsdIwcL.zjgouSOCyZdsFiinBGN1Clo-46ZUop5QUBz" + }, + { + "disabled": true, + "description": { + "content": "Refresh token from a previous token response (required for refresh_token).", + "type": "text/plain" + }, + "key": "refresh_token", + "value": "consectetur tempor c" + }, + { + "disabled": true, + "description": { + "content": "PKCE code verifier (required when authorization request included code_challenge). Raw value that was used to generate the code_challenge.", + "type": "text/plain" + }, + "key": "code_verifier", + "value": "Duis" + }, + { + "disabled": true, + "description": { + "content": "Optional scope (e.g. sp:scope:all).", + "type": "text/plain" + }, + "key": "scope", + "value": "esse" + } + ] + } + }, + "status": "OK", + "code": 200, + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": "{\n \"access_token\": \"qui amet\",\n \"token_type\": \"bearer\",\n \"expires_in\": 48164213,\n \"refresh_token\": \"velit in\",\n \"scope\": \"culpa dolor do\"\n}", + "cookie": [], + "_postman_previewlanguage": "json" + } + ], + "event": [], + "protocolProfileBehavior": { + "disableBodyPruning": true + } + }, + { + "id": "8f27ac5a-00c0-49f7-a872-acd8ba9a2fe7", + "name": "Introspect OAuth token (RFC 7662)", + "request": { + "name": "Introspect OAuth token (RFC 7662)", + "description": { + "content": "Standard OAuth 2.0 token introspection (RFC 7662). Validates an access or refresh token and returns its claims and metadata. Send the token in the request body as the \"token\" parameter.", + "type": "text/plain" + }, + "url": { + "path": [ + "oauth", + "introspect" + ], + "host": [ + "{{baseUrl}}" + ], + "query": [], + "variable": [] + }, + "header": [ + { + "key": "Content-Type", + "value": "application/x-www-form-urlencoded" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "method": "POST", + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "disabled": true, + "description": { + "content": "(Required) The access_token or refresh_token to introspect (e.g. from the token endpoint response). In Postman, use {{accessToken}} or {{refreshToken}} if set from a previous token request.", + "type": "text/plain" + }, + "key": "token", + "value": "Duis exercitation sed esse cillum" + }, + { + "disabled": true, + "description": { + "content": "Optional hint indicating the type of token being passed (RFC 7662).", + "type": "text/plain" + }, + "key": "token_type_hint", + "value": "access_token" + } + ] + } + }, + "response": [ + { + "id": "4a37ad4b-e755-41aa-b836-5c7e589cc2ee", + "name": "Token introspection result; when active is true, response includes tenant, identity, and authorization details.", + "originalRequest": { + "url": { + "path": [ + "oauth", + "introspect" + ], + "host": [ + "{{baseUrl}}" + ], + "query": [], + "variable": [] + }, + "header": [ + { + "key": "Content-Type", + "value": "application/x-www-form-urlencoded" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "method": "POST", + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "disabled": true, + "description": { + "content": "(Required) The access_token or refresh_token to introspect (e.g. from the token endpoint response). In Postman, use {{accessToken}} or {{refreshToken}} if set from a previous token request.", + "type": "text/plain" + }, + "key": "token", + "value": "Duis exercitation sed esse cillum" + }, + { + "disabled": true, + "description": { + "content": "Optional hint indicating the type of token being passed (RFC 7662).", + "type": "text/plain" + }, + "key": "token_type_hint", + "value": "access_token" + } + ] + } + }, + "status": "OK", + "code": 200, + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": "{\n \"active\": true,\n \"tenant_id\": \"urn:uuid:873b39aa-a18d-b46f-e571-bacc33fa66d5\",\n \"sub\": \"adipisicing elit ut\",\n \"pod\": \"commodo proident in\",\n \"org\": \"nisi tempor minim nulla\",\n \"identity_id\": \"aliquip deserunt proident sint\",\n \"user_name\": \"Duis aliqua officia\",\n \"iss\": \"https://ptWZwWjsJERNJFMQuhinDUrJpPWG.heGe700DFL5T+5NR0\",\n \"strong_auth\": true,\n \"authorities\": [\n \"deserunt do\",\n \"adipisicing non incididunt nostrud\"\n ],\n \"client_id\": \"Duis ad\",\n \"encoded_scope\": [\n \"ullamco\",\n \"aute dolore\"\n ],\n \"strong_auth_supported\": false,\n \"scope\": [\n \"Duis tempor officia proident\",\n \"commodo anim tempor\"\n ],\n \"exp\": -17256184,\n \"jti\": \"45f4bbbc-2069-ae07-3ff1-78cead9deb61\"\n}", + "cookie": [], + "_postman_previewlanguage": "json" + } + ], + "event": [], + "protocolProfileBehavior": { + "disableBodyPruning": true + } + }, + { + "id": "da152e05-7829-42fa-b8e7-729df6e58721", + "name": "Revoke OAuth access or refresh token", + "request": { + "name": "Revoke OAuth access or refresh token", + "description": { + "content": "Revokes an OAuth 2.0 access token or refresh token. Present the token in the request body; the backing implementation places the token's JWT ID (jti) in a cache so that the gateway will no longer accept the access token for API calls, or so that the refresh token cannot be used to obtain new access tokens.\n**Availability:** This endpoint is coming soon and may not yet be exposed via the API Gateway.", + "type": "text/plain" + }, + "url": { + "path": [ + "oauth", + "revoke" + ], + "host": [ + "{{baseUrl}}" + ], + "query": [], + "variable": [] + }, + "header": [ + { + "key": "Content-Type", + "value": "application/x-www-form-urlencoded" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "method": "POST", + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "disabled": true, + "description": { + "content": "(Required) The access_token or refresh_token to revoke.", + "type": "text/plain" + }, + "key": "token", + "value": "dolore elit" + }, + { + "disabled": true, + "description": { + "content": "Optional hint indicating the type of token being revoked (RFC 7009).", + "type": "text/plain" + }, + "key": "token_type_hint", + "value": "refresh_token" + } + ] + } + }, + "response": [ + { + "id": "83bc91f4-98ca-4f94-baf4-f0e95bccd4cb", + "name": "Token revoked successfully.", + "originalRequest": { + "url": { + "path": [ + "oauth", + "revoke" + ], + "host": [ + "{{baseUrl}}" + ], + "query": [], + "variable": [] + }, + "header": [ + { + "key": "Content-Type", + "value": "application/x-www-form-urlencoded" + } + ], + "method": "POST", + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "disabled": true, + "description": { + "content": "(Required) The access_token or refresh_token to revoke.", + "type": "text/plain" + }, + "key": "token", + "value": "dolore elit" + }, + { + "disabled": true, + "description": { + "content": "Optional hint indicating the type of token being revoked (RFC 7009).", + "type": "text/plain" + }, + "key": "token_type_hint", + "value": "refresh_token" + } + ] + } + }, + "status": "OK", + "code": 200, + "header": [], + "cookie": [], + "_postman_previewlanguage": "text" + } + ], + "event": [], + "protocolProfileBehavior": { + "disableBodyPruning": true + } + }, + { + "id": "9b79abc9-1c84-4582-adba-ab865f8769bd", + "name": "Get SAML metadata by alias", + "request": { + "name": "Get SAML metadata by alias", + "description": { + "content": "Returns SAML 2.0 metadata for the Identity Security Cloud service provider. The path uses the tenant in the form {tenant}-sp so the URL is https://{{tenant}}.api.{{domain}}.com/saml/metadata/alias/{{tenant}}-sp.", + "type": "text/plain" + }, + "url": { + "path": [ + "saml", + "metadata", + "alias", + "{{tenant}}-sp" + ], + "host": [ + "{{baseUrl}}" + ], + "query": [], + "variable": [] + }, + "header": [ + { + "key": "Accept", + "value": "application/xml" + } + ], + "method": "GET", + "body": {} + }, + "response": [ + { + "id": "48f4ff58-3885-4917-b7f9-a1f56ddd1a7c", + "name": "SAML 2.0 metadata XML for the service provider.", + "originalRequest": { + "url": { + "path": [ + "saml", + "metadata", + "alias", + "{{tenant}}-sp" + ], + "host": [ + "{{baseUrl}}" + ], + "query": [], + "variable": [] + }, + "header": [ + { + "key": "Accept", + "value": "application/xml" + } + ], + "method": "GET", + "body": {} + }, + "status": "OK", + "code": 200, + "header": [ + { + "key": "Content-Type", + "value": "application/xml" + } + ], + "body": "\n(string)", + "cookie": [], + "_postman_previewlanguage": "xml" + } + ], + "event": [], + "protocolProfileBehavior": { + "disableBodyPruning": true + } + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "const domain = pm.environment.get('domain') ? pm.environment.get('domain') : pm.collectionVariables.get('domain')", + "const tokenUrl = 'https://' + pm.environment.get('tenant') + '.api.' + domain + '.com/oauth/token';", + "const clientId = pm.environment.get('clientId');", + "const clientSecret = pm.environment.get('clientSecret');", + "const scope = pm.environment.get('scope');", + "", + "const formdata = [{", + " key: 'grant_type',", + " value: 'client_credentials'", + " },", + " {", + " key: 'client_id',", + " value: clientId", + " },", + " {", + " key: 'client_secret',", + " value: clientSecret", + " }", + "];", + "", + "if (scope) {", + " formdata.push({", + " key: 'scope',", + " value: scope", + " });", + "}", + "", + "const getTokenRequest = {", + " method: 'POST',", + " url: tokenUrl,", + " body: {", + " mode: 'formdata',", + " formdata: formdata", + " }", + "};", + "", + "", + "var moment = require('moment');", + "if (!pm.environment.has('tokenExpTime')) {", + " pm.environment.set('tokenExpTime', moment());", + "}", + "", + "if (moment(pm.environment.get('tokenExpTime')) <= moment() || !pm.environment.get('tokenExpTime') || !pm.environment.get('accessToken')) {", + " var time = moment();", + " time.add(12, 'hours');", + " pm.environment.set('tokenExpTime', time);", + " pm.sendRequest(getTokenRequest, (err, response) => {", + " const jsonResponse = response.json();", + " if (response.code != 200) {", + " throw new Error(`Unable to authenticate: ${JSON.stringify(jsonResponse)}`);", + " }", + " const newAccessToken = jsonResponse.access_token;", + " pm.environment.set('accessToken', newAccessToken);", + " });", + "", + "}", + "", + "const flag = pm.collectionVariables.get('experimentalEnabled');", + "", + "if (flag === 'true') {", + " pm.request.addHeader({", + " key: 'X-SailPoint-Experimental',", + " value: 'true'", + " });", + "}", + "", + "// Add custom User-Agent header to all requests", + "pm.request.addHeader({", + " key: 'User-Agent',", + " value: 'SailPointPostmanCollection'", + "});" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "domain", + "value": "identitynow", + "type": "string" + }, + { + "key": "baseUrl", + "value": "https://{{tenant}}.api.{{domain}}.com", + "type": "string" + } + ], + "info": { + "_postman_id": "65f2643b-8da6-47f0-a824-b9b32879cba5", + "name": "SailPoint Identity Security Cloud - OAuth & SAML", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "description": { + "content": "OAuth 2.0 and SAML endpoints for authentication and token management. Use this spec to generate a Postman collection with correct URLs (tenant + domain, no version path).", + "type": "text/plain" + } + }, + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{accessToken}}", + "type": "string" + } + ] + } +} \ No newline at end of file diff --git a/scripts/build-from-common.js b/scripts/build-from-common.js new file mode 100644 index 000000000..265b7738e --- /dev/null +++ b/scripts/build-from-common.js @@ -0,0 +1,237 @@ +#!/usr/bin/env node + +/** + * Script to build Postman collections from cloud-api-client-common repository + * This allows testing changes made in cloud-api-client-common before they're merged + * + * Usage: node scripts/build-from-common.js [version] + * + * Examples: + * node scripts/build-from-common.js beta + * node scripts/build-from-common.js v3 + * node scripts/build-from-common.js oauth + * node scripts/build-from-common.js all (default) + */ + +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const COMMON_REPO_PATH = 'cloud-api-client-common/api-specs/src/main/yaml'; +const TEMP_SPECS_DIR = 'scripts/temp-specs'; +const TEMP_COLLECTIONS_DIR = 'scripts/temp-collections'; + +const VERSIONS = [ + { name: 'v3', source: 'sailpoint-api.v3.yaml', output: 'sailpoint-api-v3-test.json', variable: 'variable-v3.json' }, + { name: 'beta', source: 'sailpoint-api.beta.yaml', output: 'sailpoint-api-beta-test.json', variable: 'variable-beta.json' }, + { name: 'v2024', source: 'sailpoint-api.v2024.yaml', output: 'sailpoint-api-v2024-test.json', variable: 'variable-v2024.json' }, + { name: 'v2025', source: 'sailpoint-api.v2025.yaml', output: 'sailpoint-api-v2025-test.json', variable: 'variable-v2025.json' }, + { name: 'v2026', source: 'sailpoint-api.v2026.yaml', output: 'sailpoint-api-v2026-test.json', variable: 'variable-v2026.json' }, + { name: 'oauth', source: 'sailpoint-oauth.yaml', output: 'sailpoint-api-oauth-test.json', variable: 'variable-auth.json' }, +]; + +function ensureDir(dir) { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + console.log(`Created directory: ${dir}`); + } +} + +function cleanDir(dir) { + if (fs.existsSync(dir)) { + fs.rmSync(dir, { recursive: true, force: true }); + console.log(`Cleaned directory: ${dir}`); + } +} + +function exec(command, description) { + console.log(`\n>>> ${description}`); + console.log(` ${command}`); + try { + execSync(command, { stdio: 'inherit' }); + } catch (error) { + console.error(`Failed: ${description}`); + throw error; + } +} + +function checkDependencies() { + console.log('Checking dependencies...'); + try { + execSync('redocly --version', { stdio: 'pipe' }); + console.log('✓ redocly is installed'); + } catch { + console.error('✗ redocly is not installed. Run: npm install -g @redocly/cli'); + process.exit(1); + } + + try { + execSync('openapi2postmanv2 --version', { stdio: 'pipe' }); + console.log('✓ openapi-to-postmanv2 is installed'); + } catch { + console.error('✗ openapi-to-postmanv2 is not installed. Run: npm install -g openapi-to-postmanv2'); + process.exit(1); + } + + if (!fs.existsSync(COMMON_REPO_PATH)) { + console.error(`✗ cloud-api-client-common not found at: ${COMMON_REPO_PATH}`); + console.error(' Make sure the repository is cloned in the expected location'); + process.exit(1); + } + console.log('✓ cloud-api-client-common repository found'); +} + +function dereferenceSpec(version) { + const sourceSpec = path.join(COMMON_REPO_PATH, version.source); + const outputYaml = path.join(TEMP_SPECS_DIR, `deref-${version.name}.yaml`); + const outputJson = path.join(TEMP_SPECS_DIR, `deref-${version.name}.json`); + + if (!fs.existsSync(sourceSpec)) { + console.log(`Skipping ${version.name}: source file not found at ${sourceSpec}`); + return null; + } + + console.log(`\n=== Processing ${version.name.toUpperCase()} ===`); + + exec( + `redocly bundle ${sourceSpec} --ext yaml -o ${outputYaml}`, + `Dereferencing ${version.name} spec to YAML` + ); + + exec( + `redocly bundle ${sourceSpec} --ext json -o ${outputJson}`, + `Dereferencing ${version.name} spec to JSON` + ); + + return { yaml: outputYaml, json: outputJson }; +} + +function buildPostmanCollection(version, dereferencedSpec) { + const outputCollection = path.join(TEMP_COLLECTIONS_DIR, version.output); + + exec( + `openapi2postmanv2 -s ${dereferencedSpec.yaml} -o ${outputCollection} -p -c postman-script/openapi2postman-config.json`, + `Building Postman collection for ${version.name}` + ); + + return outputCollection; +} + +function modifyCollection(collectionPath, version) { + console.log(`\nModifying collection: ${collectionPath}`); + + const collection = JSON.parse(fs.readFileSync(collectionPath, 'utf8')); + + // Remove all auth keys and filter responses (same as modify-collection.js) + const deleteAuthKey = (obj) => { + for (const key in obj) { + if (typeof obj[key] === 'object') { + deleteAuthKey(obj[key]); + + if (key === 'response' && Array.isArray(obj[key])) { + obj[key] = obj[key].filter(response => { + return response.code >= 200 && response.code < 300; + }); + } + } + if (key === 'auth') { + delete obj[key]; + } + if (key === 'disabled') { + if (obj[key] === false) { + obj[key] = true; + } + } + } + }; + + deleteAuthKey(collection); + + // Add auth, events, and variables + collection.auth = JSON.parse(fs.readFileSync('postman-script/base-auth.json', 'utf8')); + collection.event = JSON.parse(fs.readFileSync('postman-script/pre-script.json', 'utf8')); + + const variableFile = `postman-script/${version.variable}`; + if (fs.existsSync(variableFile)) { + collection.variable = JSON.parse(fs.readFileSync(variableFile, 'utf8')); + } + + fs.writeFileSync(collectionPath, JSON.stringify(collection, null, 2)); + console.log(`✓ Collection modified successfully`); +} + +function main() { + const args = process.argv.slice(2); + const requestedVersion = args[0] || 'all'; + + console.log('============================================'); + console.log('Build Postman Collections from Common Repo'); + console.log('============================================\n'); + + checkDependencies(); + + // Clean and create temp directories + cleanDir(TEMP_SPECS_DIR); + cleanDir(TEMP_COLLECTIONS_DIR); + ensureDir(TEMP_SPECS_DIR); + ensureDir(TEMP_COLLECTIONS_DIR); + + // Determine which versions to build + let versionsToBuild = VERSIONS; + if (requestedVersion !== 'all') { + versionsToBuild = VERSIONS.filter(v => v.name === requestedVersion); + if (versionsToBuild.length === 0) { + console.error(`Invalid version: ${requestedVersion}`); + console.error(`Valid versions: ${VERSIONS.map(v => v.name).join(', ')}, all`); + process.exit(1); + } + } + + console.log(`Building collections for: ${versionsToBuild.map(v => v.name).join(', ')}\n`); + + // Process each version + const results = []; + for (const version of versionsToBuild) { + try { + const dereferencedSpec = dereferenceSpec(version); + if (!dereferencedSpec) continue; + + const collectionPath = buildPostmanCollection(version, dereferencedSpec); + modifyCollection(collectionPath, version); + + results.push({ + version: version.name, + collection: collectionPath, + spec: dereferencedSpec.yaml + }); + } catch (error) { + console.error(`\n✗ Failed to build ${version.name}: ${error.message}`); + } + } + + // Print summary + console.log('\n============================================'); + console.log('Build Summary'); + console.log('============================================\n'); + + if (results.length === 0) { + console.log('No collections were built successfully.'); + } else { + console.log('Successfully built collections:'); + results.forEach(result => { + console.log(`\n${result.version}:`); + console.log(` Collection: ${result.collection}`); + console.log(` Spec: ${result.spec}`); + }); + + console.log('\n\nTo import into Postman:'); + results.forEach(result => { + console.log(` - Import ${result.collection}`); + }); + + console.log('\n\nTo clean up temporary files:'); + console.log(` rm -rf ${TEMP_SPECS_DIR} ${TEMP_COLLECTIONS_DIR}`); + } +} + +main();