[Pitch] Package Registry Authentication

Hello,

A package registry service as proposed in SE-0292 (API spec) may require authentication for some or all of its API in order to identify user performing the action and authorize the request accordingly.

Motivation

Common authentication methods used by web services include basic authentication, access token, and OAuth. SwiftPM supports only basic authentication today, which limits its abilities to interact with package registry services.

Proposed solution

We propose to modify the swift package-registry command and registry configuration to add token authentication support. The changes should also ensure there is flexibility to add other authentication methods in the future.

The design draws inspiration from docker login and npm login, in that there will be a single command for user to verify and persist registry credentials.

Detailed design

Changes to swift package-registry command

Instead of the swift package-registry set subcommand and the --login and --password options as proposed in SE-0292 originally, we propose the new login and logout subcommands for adding/removing registry credentials.

New login subcommand

Log in to a package registry. SwiftPM will verify the credentials using the registry service's login API. If it returns a successful response, credentials will be persisted to the operating system's credential store if supported, or the user-level .netrc file otherwise. The global configuration file located at ~/.swiftpm/configuration/registries.json will also be updated.

SYNOPSIS
    swift package-registry login <url> [options]
OPTIONS:  
  --username     Username
  --password     Password
  
  --token        Access token

  --no-confirm    Allow writing to .netrc file without confirmation
  --netrc-file   Specify the .netrc file path

url should be the registry's base URL (e.g., https://example-registry.com). In case the location of the login API is something other than /login (e.g., https://example-registry.com/api/v1/login), provide the full URL.

The table below shows the supported authentication types and their required option(s):

Authentication Method Required Option(s)
Basic --username, --password
Token --token

The tool will analyze the provided options to determine the authentication type and prompt (i.e., interactive mode) for the password/token if it is missing. For example, if only --username is present, the tool assumes basic authentication and prompts for the password.

For non-interactive mode, simply provide the --password or --token option as required or make sure the secret is present in credential storage.

If the operating system's credential store is not supported, the tool will prompt user for confirmation before writing credentials to the less secured .netrc file. Use --no-confirm to disable this confirmation.

Example: basic authentication (macOS, interactive)
> swift package-registry login https://example-registry.com \
    --username jappleseed
Enter password for 'jappleseed':

Login successful. Credentials have been saved to the operating system's secure credential store.

An entry for example-registry.com would be added to Keychain.

registries.json would be updated to indicate that example-registry.com requires basic authentication:

{
  "authentication": {
    "example-registry.com": {
      "type": "basic"
    },
    ...
  },
  ...
}
Example: basic authentication (non-macOS, interactive)
> swift package-registry login https://example-registry.com \
    --username jappleseed
Enter password for 'jappleseed':

Login successful.

WARNING: Secure credential storage is not supported on this platform. 
Your credentials will be written out to ~/.netrc. 
Continue? (Y/N): Y

Credentials have been saved to ~/.netrc.

An entry for example-registry.com would be added to the .netrc file:

machine example-registry.com
login jappleseed
password alpine

registries.json would be updated to indicate that example-registry.com requires basic authentication:

{
  "authentication": {
    "example-registry.com": {
      "type": "basic"
    },
    ...
  },
  ...
}
Example: basic authentication (non-macOS, non-interactive)
> swift package-registry login https://example-registry.com \
    --username jappleseed \
    --password alpine
    --no-confirm
    
Login successful. Credentials have been saved to ~/.netrc.

An entry for example-registry.com would be added to the .netrc file:

machine example-registry.com
login jappleseed
password alpine

registries.json would be updated to indicate that example-registry.com requires basic authentication:

{
  "authentication": {
    "example-registry.com": {
      "type": "basic"
    },
    ...
  },
  ...
}
Example: basic authentication (non-macOS, non-interactive, non-default login URL)
> swift package-registry login https://example-registry.com/api/v1/login \
    --username jappleseed \
    --password alpine
    --no-confirm
    
Login successful. Credentials have been saved to ~/.netrc.

An entry for example-registry.com would be added to the .netrc file:

machine example-registry.com
login jappleseed
password alpine

registries.json would be updated to indicate that example-registry.com requires basic authentication:

{
  "authentication": {
    "example-registry.com": {
      "type": "basic",
      "loginAPIPath": "/api/v1/login"
    },
    ...
  },
  ...
}
Example: token authentication
> swift package-registry login https://example-registry.com \
    --token jappleseedstoken

An entry for example-registry.com would be added to the operating system's credential store if supported, or the user-level .netrc file otherwise:

machine example-registry.com
login token
password jappleseedstoken

registries.json would be updated to indicate that example-registry.com requires token authentication:

{
  "authentication": {
    "example-registry.com": {
      "type": "token"
    },
    ...
  },
  ...
}

New logout subcommand

Log out from a registry. Credentials are removed from the operating system's credential store if supported, and the global configuration file (registries.json).

To avoid accidental removal of sensitive data, .netrc file needs to be updated manually by the user.

SYNOPSIS
    swift package-registry logout <url>

Changes to registry configuration

We will introduce a new authentication key to the global registries.json file, which by default is located at ~/.swiftpm/configuration/registries.json. Any package registry that requires authentication must have a corresponding entry in this dictionary.

{
  "registries": {
    "[default]": {
      "url": "https://example-registry.com"
    }
  },
  "authentication": {
    "example-registry.com": {
      "type": <AUTHENTICATION_TYPE>, // One of: "basic", "token"
      "loginAPIPath": <LOGIN_API_PATH> // Optional. Overrides the default API path (i.e., /login).
    }
  },
  "version": 1
}

type must be one of the following:

  • basic: username and password
  • token: access token

Credentials are to be specified in the native credential store of the operating system if supported, otherwise in the user-level .netrc file. (Only macOS Keychain will be supported in the initial feature release; more might be added in the future.)

See credential storage for more details on configuring credentials for each authentication type.

Credential storage

Basic Authentication

macOS Keychain

Registry credentials should be stored as "Internet password" items in the macOS Keychain. The "item name" should be the registry URL, including https:// (e.g., https://example-registry.com).

.netrc file (non-macOS platforms only)

A .netrc entry for basic authentication looks as follows:

machine example-registry.com
login jappleseed
password alpine

By default, SwiftPM looks for .netrc file in the user's home directory. A custom .netrc file can be specified using the --netrc-file option.

Token Authentication

User can configure access token for a registry as similarly done for basic authentication, but with token as the login/username and the access token as the password.

For example, a .netrc entry would look like:

machine example-registry.com
login token
password jappleseedstoken

Additional changes in SwiftPM

  1. Only the user-level .netrc file will be used. Project-level .netrc file will not be supported.
  2. SwiftPM will perform lookups in one credential store only. For macOS, it will be Keychain. For all other platforms, it will be the user-level .netrc file.
  3. The --disable-keychain and --disable-netrc options will be removed.

New package registry service API

A package registry that requires authentication must implement the new API endpoint(s) covered in this section.

login API

SwiftPM will send a HTTP POST request to /login to validate user credentials provided by the login subcommand. The request will include an Authorization HTTP header constructed as follows:

  • Basic authentication: Authorization: Basic <base64 encoded username:password>
  • Token authentication: Authorization: Bearer <token>

The registry service must return HTTP status 200 in the response if login is successful, and 401 otherwise.

In case the registry service does not support an authentication method, it should return HTTP status 501.

SwiftPM will persist user credentials to local credential store if login is successful.

Security

This proposal moves SwiftPM to use operating system's native credential store (e.g., macOS Keychain) on supported platforms, which should yield better security.

We are also eliminating the use of project-level .netrc file. This should prevent accidental checkin of .netrc file and thus leakage of sensitive information.

Impact on existing packages

This proposal eliminates the project-level .netrc file. There should be no other impact on existing packages.

9 Likes

To maximum security, I don’t think defaulting to .netrc on all non-macOS platforms is a wise choice. .netrc could be a fallback for all platforms, or we can provide a new key in registries.json to pick a credential manager.

On Windows, there’s a built-in Windows credential store which can be accessed within WinCred.h; on Linux, it’s possible to visit a keystore with GNOME’s libsecret. We should always prefer more secure way of handling credentials.

2 Likes

In general this pitch looks good to me and including token based authentication is a welcome addition.

I think of global configuration as affecting all users on a machine. Isn't that a user-level configuration file?

It isn't clear to me how the client determines whether or not to append /login. I can think of a few approaches:

  1. append /login if the URL doesn't end in /login
  2. append /login when the URL only contains a host name with no path
  3. the client tries both, e.g. first append /login and make request and if that doesn't succeed then try without.

Option 1 may not provide the flexibility you're looking for (e.g. by "something other than /login" do you intend to allow something like https://example-registry.com/user/signin?).

Option 2 requires the user to include /login for any registry that isn't based at the root of a host (e.g. if my registry's base URL is https://registries.some-cloud-provider.com/my-account/my-registry then I'd have to add /login).

I'm also curious what the login command adds to the registries.json configuration file. Does it only add to the authentication key? Or does it also add to the registries key? If the registry's base URL includes a path like my previous example, what goes in the authentication key, a key named registries.some-cloud-provider.com or registries.some-cloud-provider.com/my-account/my-registry?

It also appears to me that the swift client will only support a single set of credentials for a given host name. If the same host name hosts multiple registries (as in the case of a cloud provider) then a user cannot use different credentials for those registries, am I correct?

1 Like

+1 I think the proposal should state that clearly - that it should prefer the most secure way of handling credentials for the platform (with the obvious example of the keychain on macOS), and that .netrc is a fallback for those cases (platforms) where there is not a clear solution. of course, we would need to debate the details of that per platform, but IMO that is an implementation detail that goes beyond the proposal which is setting the tone conceptually.

4 Likes

Should we show a warning if using authentication on an insecure connection?

Also:

I think it would be good to show a message if any relevant credentials are left in the credential storage after the user has logged out; users might expect that this data has been wiped (and on some systems, that will be correct). If we expect them to remove that data manually, we should also tell them that.

3 Likes

Agreed, It is the intention to always use the most secured way supported on the platform, but I can see how this isn't being clear in the current wording of the proposal. I will update it as Tom has suggested.

3 Likes

It's user's global configuration. I will make this more consistent with common terminology.

It's option 2, but if the URL has a path, which doesn't have to be /login, the client will record the custom path.

So in this example (e.g., https://registries.some-cloud-provider.com/my-account/my-registry/my-login), the config would look something like:

{
  "authentication": {
    "registries.some-cloud-provider.com": {
      "type": "basic",
      "loginAPIPath": "/my-account/my-registry/my-login"
    },
    ...
  },
  ...
}

I've just realized that the proposal makes /login a requirement for implementing registries. This isn't true. A registry must have some login API, but it doesn't have to be at /login and user can override it (as shown in the example above). I will update that section of the proposal.

The current design adds to authentication only.

That's correct. Are you alluding to having resource-based (i.e. scope/package) auth?

1 Like

Yep, we will do that.

3 Likes

Hi how it could be work, when you try resolve dependencies with Xcode ?
In new Xcode tools (btw 14.3) i see opportunity how resolve dependency with auth only by the command line
How can i setup Xcode for using auth ?

Xcode doesn't support SPM registries in its native integration (I assume it supports them if you simply open a Package.swift file directly). You can only use SPM directly, as outlined in this pitch and the merged proposal.

Really? i don't understand that. Even you can resolve dependencies by command line, after that you won't be able to build your project.
Btw Xcode support half of this resolution. At least now xcode supporting registry without credential, and nothing prevents you build project with self hosted spm registry.
It seems like half-resolved solution
Thank for answer

Credentials supplied to swift package-registry login are saved to credential store (i.e., Keychain in macOS) or netrc file. During registry dependency resolution/download these credentials will be read and applied automatically, whether the operation is done with SwiftPM command-line or Xcode.

2 Likes

Hi, yes, i though that must works like that, i checked both places (netrc and keychain), all of them contains credentials. But when Xcode try to resolve package dependencies, it doesn't use credentials, it is confused me :confused:
Maybe i need to setup Xcode for this ?

Maybe i need to setup Xcode for this ?

No I don't think you need to. Can you please file a bug and we will take a look?

1 Like

Of course, can you please redirect me to place where i can do it ?

1 Like