diff --git a/api/base.yaml b/api/base.yaml index b785a21..065ef94 100644 --- a/api/base.yaml +++ b/api/base.yaml @@ -64,6 +64,35 @@ tags: - `/oidc/token/exchange` endpoint: Only accepts `oidcIdToken` - Package publishing: May accept `npmSessionToken`, `npmAccessToken`, or `oidcExchangeToken` + **Granular Access Tokens with `bypass_2fa`:** + + Granular Access Tokens (GATs) created with `bypass_2fa: true` are + allowed to perform automation flows such as direct publish, but they + are **not** permitted to perform account-identity or governance write + operations. Requests to those endpoints authenticated with a + bypass-2FA GAT are rejected with `403 Forbidden` and must instead be + performed via an interactive 2FA challenge (npm website, or an npm + session token / non-bypass GAT with a valid `npm-otp`). + + Operations rejected for bypass-2FA GATs include (non-exhaustive): + + - Token management — `POST /-/npm/v1/tokens`, `DELETE /-/npm/v1/tokens/token/{token}` + - Account settings — change password, change email, change 2FA + configuration, generate recovery codes, update profile + - Package governance — `POST /-/package/{escapedPackageName}/access`, + `POST /-/package/{package}/trust`, + `DELETE /-/package/{package}/trust/{config-uuid}`, + maintainer edits, and package/binary deletion (unpublish) + - Org and team governance — + `PUT`/`DELETE /-/org/{orgName}/user`, + `PUT /-/org/{orgName}/team`, + `DELETE /-/org/{orgName}/{teamName}`, + `PUT`/`DELETE /-/org/{orgName}/{teamName}/user`, + `PUT`/`DELETE /-/team/{orgName}/{teamName}/package` + + Direct publish (`PUT /{escapedPackageName}`) remains functional for + bypass-2FA GATs. + - name: Tokens x-displayName: Tokens description: | @@ -131,6 +160,11 @@ components: Granular Access Token (GAT) with fine-grained permissions. These tokens can be scoped to specific packages and operations. + GATs created with `bypass_2fa: true` cannot perform account, + token, or org/team/package governance write operations — those + requests are rejected with `403 Forbidden`. See the Authentication + section above for the full list. + # Alias for npmAccessToken - for backward compatibility granularAccessToken: type: http @@ -140,6 +174,10 @@ components: These tokens can be scoped to specific packages and operations. (Alias for npmAccessToken) + GATs created with `bypass_2fa: true` cannot perform account, + token, or org/team/package governance write operations — those + requests are rejected with `403 Forbidden`. + # Short-lived tokens obtained from OIDC token exchange oidcExchangeToken: type: http diff --git a/api/registry.npmjs.com/access.yaml b/api/registry.npmjs.com/access.yaml index b9f2373..222a2e8 100644 --- a/api/registry.npmjs.com/access.yaml +++ b/api/registry.npmjs.com/access.yaml @@ -56,6 +56,12 @@ paths: tags: - Access summary: Grant access to a package for a team + description: | + Grant a team access to a package. + + Granular Access Tokens with `bypass_2fa: true` are not permitted to + call this endpoint and receive `403 Forbidden`. This operation must + be performed via an interactive 2FA challenge. operationId: createTeamPackageGrant requestBody: required: true @@ -75,23 +81,37 @@ paths: description: The access level of the package to grant to the team security: - npmSessionToken: [] + - npmAccessToken: [] + - granularAccessToken: [] responses: '201': $ref: './api/shared-components.yaml#/components/responses/EmptySuccess' '401': $ref: './api/shared-components.yaml#/components/responses/Unauthorized' + '403': + $ref: './api/shared-components.yaml#/components/responses/Forbidden2FABypassGAT' delete: tags: - Access summary: Remove access to a package for a team + description: | + Remove a team's access to a package. + + Granular Access Tokens with `bypass_2fa: true` are not permitted to + call this endpoint and receive `403 Forbidden`. This operation must + be performed via an interactive 2FA challenge. operationId: deleteTeamPackageGrant security: - npmSessionToken: [] + - npmAccessToken: [] + - granularAccessToken: [] responses: '204': $ref: './api/shared-components.yaml#/components/responses/EmptySuccess' '401': $ref: './api/shared-components.yaml#/components/responses/Unauthorized' + '403': + $ref: './api/shared-components.yaml#/components/responses/Forbidden2FABypassGAT' /-/org/{orgName}/package: parameters: - $ref: './api/shared-components.yaml#/components/parameters/OrgName' @@ -148,6 +168,12 @@ paths: tags: - Access summary: Sets the various access levels for a package. + description: | + Set package visibility and 2FA/automation policy for a package. + + Granular Access Tokens with `bypass_2fa: true` are not permitted to + call this endpoint and receive `403 Forbidden`. This operation must + be performed via an interactive 2FA challenge. operationId: setPackageAccess requestBody: required: true @@ -171,8 +197,12 @@ paths: description: Whether or not automation tokens override the requirement for multifactor auth security: - npmSessionToken: [] + - npmAccessToken: [] + - granularAccessToken: [] responses: '200': $ref: './api/shared-components.yaml#/components/responses/EmptySuccess' '401': $ref: './api/shared-components.yaml#/components/responses/Unauthorized' + '403': + $ref: './api/shared-components.yaml#/components/responses/Forbidden2FABypassGAT' diff --git a/api/registry.npmjs.com/org.yaml b/api/registry.npmjs.com/org.yaml index 9503fd2..03d612a 100644 --- a/api/registry.npmjs.com/org.yaml +++ b/api/registry.npmjs.com/org.yaml @@ -60,10 +60,17 @@ paths: tags: - Org summary: Set user membership in an org - description: Set a user's membership in an org. If the user is not already a member, an invite will be sent. + description: | + Set a user's membership in an org. If the user is not already a member, an invite will be sent. + + Granular Access Tokens with `bypass_2fa: true` are not permitted to + call this endpoint and receive `403 Forbidden`. This operation must + be performed via an interactive 2FA challenge. operationId: changeOrgMembership security: - npmSessionToken: [] + - npmAccessToken: [] + - granularAccessToken: [] requestBody: required: true content: @@ -86,14 +93,23 @@ paths: $ref: '#/components/responses/OrgInvite' "401": $ref: './api/shared-components.yaml#/components/responses/Unauthorized' + "403": + $ref: './api/shared-components.yaml#/components/responses/Forbidden2FABypassGAT' delete: tags: - Org summary: Remove user membership in an org - description: Remove a user's membership in an org + description: | + Remove a user's membership in an org. + + Granular Access Tokens with `bypass_2fa: true` are not permitted to + call this endpoint and receive `403 Forbidden`. This operation must + be performed via an interactive 2FA challenge. operationId: deleteOrgMembership security: - npmSessionToken: [] + - npmAccessToken: [] + - granularAccessToken: [] requestBody: required: true content: @@ -109,3 +125,5 @@ paths: $ref: './api/shared-components.yaml#/components/responses/EmptySuccess' "401": $ref: './api/shared-components.yaml#/components/responses/Unauthorized' + "403": + $ref: './api/shared-components.yaml#/components/responses/Forbidden2FABypassGAT' diff --git a/api/registry.npmjs.com/team.yaml b/api/registry.npmjs.com/team.yaml index e036cf2..603f401 100644 --- a/api/registry.npmjs.com/team.yaml +++ b/api/registry.npmjs.com/team.yaml @@ -46,7 +46,12 @@ paths: tags: - Team summary: Create a new team - description: Create a new team for an org + description: | + Create a new team for an org. + + Granular Access Tokens with `bypass_2fa: true` are not permitted to + call this endpoint and receive `403 Forbidden`. This operation must + be performed via an interactive 2FA challenge. operationId: putScopeTeam requestBody: required: true @@ -64,6 +69,8 @@ paths: example: { name: 'wombats', description: 'All developers' } security: - npmSessionToken: [] + - npmAccessToken: [] + - granularAccessToken: [] responses: '201': description: Team was created successfully @@ -79,6 +86,8 @@ paths: name: wombats '401': $ref: './api/shared-components.yaml#/components/responses/Unauthorized' + '403': + $ref: './api/shared-components.yaml#/components/responses/Forbidden2FABypassGAT' /-/org/{orgName}/{teamName}: parameters: - $ref: './api/shared-components.yaml#/components/parameters/OrgName' @@ -88,15 +97,24 @@ paths: tags: - Team summary: Delete a team - description: Delete a team from a given org + description: | + Delete a team from a given org. + + Granular Access Tokens with `bypass_2fa: true` are not permitted to + call this endpoint and receive `403 Forbidden`. This operation must + be performed via an interactive 2FA challenge. operationId: deleteTeam security: - npmSessionToken: [] + - npmAccessToken: [] + - granularAccessToken: [] responses: '204': $ref: './api/shared-components.yaml#/components/responses/EmptySuccess' '401': $ref: './api/shared-components.yaml#/components/responses/Unauthorized' + '403': + $ref: './api/shared-components.yaml#/components/responses/Forbidden2FABypassGAT' /-/org/{orgName}/{teamName}/user: parameters: - $ref: './api/shared-components.yaml#/components/parameters/OrgName' @@ -119,7 +137,12 @@ paths: tags: - Team summary: Add a user to a team - description: Add a user to a team in an org. The user must already be a member of the org. + description: | + Add a user to a team in an org. The user must already be a member of the org. + + Granular Access Tokens with `bypass_2fa: true` are not permitted to + call this endpoint and receive `403 Forbidden`. This operation must + be performed via an interactive 2FA challenge. operationId: createTeamMembership requestBody: required: true @@ -135,15 +158,25 @@ paths: user: npm-cli-bot security: - npmSessionToken: [] + - npmAccessToken: [] + - granularAccessToken: [] responses: '201': $ref: './api/shared-components.yaml#/components/responses/EmptySuccess' '401': $ref: './api/shared-components.yaml#/components/responses/Unauthorized' + '403': + $ref: './api/shared-components.yaml#/components/responses/Forbidden2FABypassGAT' delete: tags: - Team summary: Remove a user from a team + description: | + Remove a user from a team. + + Granular Access Tokens with `bypass_2fa: true` are not permitted to + call this endpoint and receive `403 Forbidden`. This operation must + be performed via an interactive 2FA challenge. operationId: deleteTeamMembership requestBody: required: true @@ -159,8 +192,12 @@ paths: user: npm-cli-bot security: - npmSessionToken: [] + - npmAccessToken: [] + - granularAccessToken: [] responses: '204': $ref: './api/shared-components.yaml#/components/responses/EmptySuccess' '401': $ref: './api/shared-components.yaml#/components/responses/Unauthorized' + '403': + $ref: './api/shared-components.yaml#/components/responses/Forbidden2FABypassGAT' diff --git a/api/registry.npmjs.com/token.yaml b/api/registry.npmjs.com/token.yaml index 9a72410..fef455f 100644 --- a/api/registry.npmjs.com/token.yaml +++ b/api/registry.npmjs.com/token.yaml @@ -14,6 +14,9 @@ paths: - If 2FA is enabled on your account, provide the OTP via the `npm-otp` header - If 2FA is not enabled, an email OTP will be sent and must be provided via the `npm-otp` header - For WebAuthn users, the OTP is returned in the `doneUrl` after authentication + - **Granular Access Tokens with `bypass_2fa: true` are not permitted** + to call this endpoint and receive `403 Forbidden`. Token creation + (and escalation) requires an interactive 2FA challenge. **Important notices:** - All responses include security notices via the `npm-notice` header regarding token limitations @@ -86,7 +89,18 @@ paths: description: 'Expiration in days (number) or ISO date string. Read-write tokens: maximum 90 days, defaults to 7 days. Read-only tokens: unlimited maximum, defaults to 30 days' bypass_2fa: type: boolean - description: Allow token to bypass 2FA requirements + description: | + Allow token to bypass 2FA requirements for automation + flows such as direct publish. + + Tokens created with `bypass_2fa: true` **cannot** perform + account, token, or org/team/package governance write + operations (for example: creating or deleting tokens, + changing package access or trust configuration, + adding/removing org or team members, granting/revoking + team package access, unpublishing). Those requests are + rejected with `403 Forbidden` and must be performed via + an interactive 2FA challenge. default: false cidr: type: array @@ -122,6 +136,8 @@ paths: - name security: - npmSessionToken: [] + - npmAccessToken: [] + - granularAccessToken: [] responses: "201": description: "Token created successfully" @@ -165,7 +181,11 @@ paths: nullable: true bypass_2fa: type: boolean - description: Indicates if the token can bypass 2FA requirements. + description: | + Indicates if the token can bypass 2FA requirements for + automation flows. When `true`, the token is rejected + (`403 Forbidden`) by account, token, and org/team/package + governance write endpoints. revoked: type: string format: date-time @@ -265,6 +285,8 @@ paths: error: 'Read-write tokens cannot have expiration longer than 90 days' "401": $ref: '#/components/responses/UnauthorizedWithWebAuth' + "403": + $ref: './api/shared-components.yaml#/components/responses/Forbidden2FABypassGAT' "500": description: Internal server error @@ -359,7 +381,12 @@ paths: description: Indicates if the token has readonly permissions bypass_2fa: type: boolean - description: Indicates if the token can bypass 2FA requirements + description: | + Indicates if the token can bypass 2FA + requirements for automation flows. When `true`, + the token is rejected (`403 Forbidden`) by + account, token, and org/team/package governance + write endpoints. cidr: type: array items: @@ -457,6 +484,9 @@ paths: **Requirements:** - Must be authenticated with a valid Bearer token - May require 2FA OTP depending on user settings + - **Granular Access Tokens with `bypass_2fa: true` are not permitted** + to call this endpoint and receive `403 Forbidden`. Token deletion + requires an interactive 2FA challenge. **Web authentication flow:** - When `npm-auth-type=web` and `npm-command=token` headers are present and 2FA is required, @@ -509,6 +539,8 @@ paths: this is a token deletion command. security: - npmSessionToken: [] + - npmAccessToken: [] + - granularAccessToken: [] responses: "204": description: Token deleted successfully @@ -586,6 +618,8 @@ paths: value: authUrl: "https://www.npmjs.com/auth/cli/00000000-0000-0000-0000-000000000000" doneUrl: "https://registry.npmjs.org/-/v1/done?authId=00000000-0000-0000-0000-000000000000" + "403": + $ref: './api/shared-components.yaml#/components/responses/Forbidden2FABypassGAT' "404": description: Not found - Token does not exist content: diff --git a/api/registry.npmjs.com/trust.yaml b/api/registry.npmjs.com/trust.yaml index 17bbad4..6e15b51 100644 --- a/api/registry.npmjs.com/trust.yaml +++ b/api/registry.npmjs.com/trust.yaml @@ -210,6 +210,9 @@ paths: - User MUST have write permission to the package - MUST have 2FA enabled on their account - User MUST be authenticated + - Granular Access Tokens with `bypass_2fa: true` are **not** permitted + to call this endpoint and receive `403 Forbidden`. Configuring + trusted publishers requires an interactive 2FA challenge. operationId: configureTrustedPublisher parameters: @@ -400,7 +403,7 @@ paths: authUrl: "https://www.npmjs.com/auth/cli/00000000-0000-0000-0000-000000000000" doneUrl: "https://registry.npmjs.org/-/v1/done?authId=00000000-0000-0000-0000-000000000000" "403": - description: 2fa required + description: 2fa required, insufficient permissions, or bypass-2FA GAT content: application/json: schema: @@ -408,11 +411,17 @@ paths: properties: message: type: string + error: + type: string examples: 2fa_disabled: summary: "Please enable 2fa for your account" value: message: "Please enable 2fa for your account" + bypass_2fa_gat: + summary: "Bypass-2FA Granular Access Token not permitted" + value: + error: "Granular access tokens that bypass two-factor authentication may not perform this action. For more info, visit: https://gh.io/npm-gat-bypass2fa-deprecation" "404": description: Package not found content: @@ -457,6 +466,10 @@ paths: - User MUST have write permission to the package - MUST have 2FA enabled on their account - User MUST be authenticated + - Granular Access Tokens with `bypass_2fa: true` are **not** permitted + to call this endpoint and receive `403 Forbidden`. Deleting a + trusted publisher configuration requires an interactive 2FA + challenge. operationId: deleteTrustedPublisher parameters: @@ -564,7 +577,7 @@ paths: authUrl: "https://www.npmjs.com/auth/cli/00000000-0000-0000-0000-000000000000" doneUrl: "https://registry.npmjs.org/-/v1/done?authId=00000000-0000-0000-0000-000000000000" "403": - description: 2fa required or insufficient permissions + description: 2fa required, insufficient permissions, or bypass-2FA GAT content: application/json: schema: @@ -572,6 +585,8 @@ paths: properties: message: type: string + error: + type: string examples: 2fa_disabled: summary: "Please enable 2fa for your account" @@ -581,6 +596,10 @@ paths: summary: "Insufficient permissions to delete configuration" value: message: "Insufficient permissions to access trusted publisher config for the package" + bypass_2fa_gat: + summary: "Bypass-2FA Granular Access Token not permitted" + value: + error: "Granular access tokens that bypass two-factor authentication may not perform this action. For more info, visit: https://gh.io/npm-gat-bypass2fa-deprecation" "404": description: Package or configuration not found content: diff --git a/api/shared-components.yaml b/api/shared-components.yaml index 4225329..3331a22 100644 --- a/api/shared-components.yaml +++ b/api/shared-components.yaml @@ -65,3 +65,15 @@ components: description: Error message example: error: Missing "Bearer" header. + Forbidden2FABypassGAT: + description: 'Forbidden: bypass-2FA GAT not permitted for account or governance writes' + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Error message + example: + error: 'Granular access tokens that bypass two-factor authentication may not perform this action. For more info, visit: https://gh.io/npm-gat-bypass2fa-deprecation'