If authentication is required, Julia uses bearer tokens (RFC 6750) to authenticate package server requests.
That is, the HTTP requests set the Authorization: Bearer $(access_token) header when fetching data from the package server.
PkgAuthentication.jl manages acquiring these tokens from package server, generally via an interactive flow. This document describes the protocols related to the authentication, and also acts as a specification for a few PkgAuthentication-specific conventions that authenticated package servers should follow.
Additional resources & references:
Note: the discussions of the package server protocol itself (i.e. downloading of registires, packages, and artifacts) is out of scope of this document.
Julia (i.e. Pkg.jl - the package manager) stores the token information in a auth.toml file in the "server directory" at ~/.julia/servers/{hostname}/auth.toml.
Pkg uses the following top-level key values pairs:
access_token(REQUIRED): the bearer token used to authorize normal requests (string)expires_at(OPTIONAL): an absolute expiration time (seconds from UNIX epoch; integer)expires_in(OPTIONAL): a relative expiration time (seconds; integer)refresh_url(OPTIONAL): URL to fetch a new token from (string)refresh_token(OPTIONAL): bearer token used to authorize refresh requests (string)
The auth.toml file may contain other fields (e.g. a username, or user email), but they are ignored by Pkg.
The two other fields mentioned in RFC6750 are token_type and scope.
These are omitted since only Bearer tokens are currently supported, and the scope is always implicitly to provide access to Pkg protocol URLs.
Pkg servers, however, SHOULD NOT send auth.toml files with token_type or scope fields, as these names may be used in the future, e.g. to support other kinds of tokens or to limit the scope of an authorization to a subset of Pkg protocol URLs.
As an example, a valid auth.toml file might look something like this:
access_token = "ey...vSA"
expires_at = 1742014471
expires_in = 86400
refresh_url = "https://juliahub.com/auth/renew/token.toml/v2/"
refresh_token = "Ch...du"Note: the server directory path can be determined with Pkg.PlatformEngines.get_server_dir.
Pkg will determine whether the access token needs to be refreshed by examining the expires_at and/or expires_in fields of the auth file.
The expiration time is the minimum of expires_at and mtime(auth_file) + expires_in.
When the Pkg client downloads a new auth.toml file, if there is a relative expires_in field, an absolute expires_at value is computed based on the client's current clock time.
This combination of policies allows expiration to work gracefully even in the presence of clock skew between the server and the client.
If the access token is expired and there are refresh_token and refresh_url fields in auth.toml, a new auth file is requested by making a request to refresh_url with an Authorization: Bearer $(refresh_token) header.
Pkg will refuse to make the refresh request unless refresh_url is an HTTPS URL.
Note that refresh_url need not be a URL on the Pkg server: token refresh can be handled by separate server.
If the request is successful and the returned auth.toml file is a well-formed TOML file with at least an access_token field, it is saved to server directory, replacing the existing auth.toml file.
Checking for access token expiry and refreshing auth.toml is done before each Pkg client request to a Pkg server.
If the auth file is updated, the new access token is used, so the token should, in theory, always be up to date.
Practice is different from theory, of course, and if the Pkg server considers the access token expired, it may return an HTTP 401 Unauthorized status code in the response.
Then, the Pkg client should attempt to refresh the auth token.
If, after attempting to refresh the access token, the server still returns HTTP 401 Unauthorized, the Pkg client server will present the body of the error response to the user or user agent (IDE).
PkgAuthentication is designed to assist the user in acquiring authentication tokens by performing an interactive, browser-based authentication flow.
The following information is necessary to start the authentication flow, to know which URL to request the token from:
pkg_server: the package server URL; i.e. the value that is used (and generally automatically determined from) theJULIA_PKG_SERVERenvironment variable.auth_suffix: specifies an additional URL suffix to append to thepkg_serverURL to form the authentication URLs. This defaults to/auth.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this specification are to be interpreted as described in RFC2119.
PkgAuthentication supports two different authentication mechanisms:
- Classic Authentication Flow
- Device Authentication Flow
When initiating the authentication flow for a brand new token, PkgAuthentication calls the package server authentication configuration endpoint at
$(pkg_server)/$(auth_suffix)/configuration
which can be used to advertise what authentication flows the server supports.
For a valid implementation of the configuration endpoint, the package server:
- MUST always return a
200HTTP status code. - The response body MUST be a valid JSON object (i.e.
{...})
If the response is invalid (non-200 code or an invalid JSON object), PkgAuthentication will assume that the server only supports the Classic Authentication Flow, and proceed accordingly.
When device authentication is not supported by the server the response body MAY contain the following JSON data:
{
"auth_flows": ["classic"]
}If the auth_flows property is present, it MUST be an array of strings.
If it is missing, it is assumed to have the value ["classic"].
In this case, PkgAuthentication will execute the Classic Authentication Flow.
When device authentication is supported by the server, the response body MUST contain the auth_flows property, and the array MUST contain the value device.
Additionally, the response body MUST contain the following properties:
device_authorization_endpoint: URL to be used to initiate the device authentication flow.device_token_endpoint: URL to be used to exchange the device code for a token.device_token_refresh_url: URL that can be used to refresh the token.
Furthermore, the response body MAY contain the following properties:
device_token_scope: Scope to be used when requesting a token. If missing, the scope will be omitted from the device token request.
An example of a possible valid response body:
{
"auth_flows": ["classic", "device"],
"device_authorization_endpoint": "https://auth.juliahub.com/auth/device/code",
"device_token_endpoint": "https://auth.juliahub.com/auth/token",
"device_token_refresh_url": "https://juliahub.com/auth/renew/token.toml/device/",
"device_token_scope": "openid email profile offline_access"
}In this case, PkgAuthentication will execute the Device Authentication Flow.
Note: URLs in the examples are only representative. Actual URLs may differ.
The classic authentication flow is similar to the OAuth 2.0 Authorization Code Grant flow, but uses different conventions for endpoints.
The flow goes through the following steps:
-
Generating an 32 byte random challenge string.
-
Requesting a challenge from the Pkg server.
POST $(pkg_server)/$(auth_suffix)/challengeThe body of the request should be the challenge string (just plain bytes, not encoded as JSON or anything).
The server MUST respond with the status code
200and a body containing the response URL fragmentresponse(again, plain bytes, no encoding of any form). -
Opening the response URL fragment in the user's browser.
At this point, the user should open the following URL in a web browser (that is logged into the package server) and approve the authentication request:
$(pkg_server)/$(auth_suffix)/response?$(response)The package server SHOULD implement a basic interface for the user to approve or deny the authentication request. It SHOULD also indicate which user is logged in and which package server is being authenticated against. When the user approves the request, it SHOULD indicate to the user that the request has been approved and that they can close the browser window and return to their application.
-
Polling the package server's token claiming endpoint.
While waiting for the user to approve the authentication request in step (3), PkgAuthentication will poll the package server's token claiming endpoint. The polling is done by sending a POST request
POST $(pkg_server)/$(auth_suffix)/claimtokenwith the following request body
{ "challenge": "$(challenge)", "response": "$(response)" }If the authentication request is valid, the server MUST respond with the status code
200. If the authentication request is invalid or expired, a non-200status code MUST be returned.If the authentication request is valid, the server MUST respond with a JSON object.
If the user has completed the interactive authentication flow in the browser, the request body MUST contain a
tokenproperty. Thetokenproperty MUST itself be a JSON object, and it minimally MUST contain anaccess_tokenvalue (which in turn contains the token value that can be used as the bearer token when performing package server requests).All the fields of the
tokenproperty will be stored in theauth.tomlfile. As such, the response MAY return additional fields, to either set the standard optionsauth.tomlfields, or any additional fields the package server deems useful.If the user has not yet completed the interactive authentication flow in the browser, the request body MAY contain an
expiryproperty, which MUST be an integer and indicates time at which the response/challenge pair will expire on the server. -
Constructing the
auth.tomlfile.If PkgAuthentication successfully acquires a token from polling the
/claimtokenendpoint, it will write the token to theauth.tomlfile. It will write out all the keys and values of thetokenin theauth.tomlfile as TOML.
Device flow authentication enables an application to authenticate a user by providing a link that can be opened on another device where the user can proceed with authentication. The application will be able to check whether the user has completed authentication on the other device by calling certain APIs. Finally, the application can retrieve the users OAuth token via the same API call. Device flow authentication becomes necessary on devices that do not have a browser based interface for regular login or applications that are not browser based such as command line applications. More details here.
The flow goes through the following steps:
-
A
POSTrequest MUST be made to thedevice_authorization_endpointwith the headersAccept: application/jsonandContent-Type: application/x-www-form-urlencoded. The body of the request MUST contain the url encodedclient_idandscopevalues.The server MUST respond with a 200 status and a body containing a JSON encoded structure. The JSON structure MUST include a
device_codeand averification_uri_completeamong other values. Example:{ "device_code": "abcdefghijklmnopqrstuvwxyz1234567890", "user_code": "FJMC-LPVR", "verification_uri": "https://juliahub.com/dex/device", "verification_uri_complete": "https://juliahub.com/dex/device?user_code=FJMC-LPVR", "expires_in": 300, "interval": 5 } -
The client should open
verfication_uri_completein the browser so that the user can login and approve the authorization request. The package server SHOULD provide an interface for the user to login and approve or deny the authorization request. -
The client should now poll for completion of the authorization request. It can do so by making a
POSTrequest to thedevice_token_endpointwith the same headers as was used for thedevice_authorization_endpointcall. The body of the request MUST contain the url encodedclient_idandscope. The values of these parameters must match the values sent fordevice_authorization_endpoint. In addition to these two parameters, agrant_typeanddevice_codeparameter must also be included with valuesurn:ietf:params:oauth:grant-type:device_codeand thedevice_coderesponse value from thedevice_authorization_endpointcall, respectively.While the user hasn't finished responding to the authorization request or has denied the authorization request, the
device_token_endpointresponse status must be401or400.When the user approves the authorization request, the
device_token_endpointresponse status must be 200 with a JSON body containing theaccess_token,id_token,refresh_tokenandexpires_in. Example:{ "access_token": "abcdefghijklmnopqrstuvwxyz1234567890", "token_type": "bearer", "expires_in": 86399, "refresh_token": "abcdefghijklmnopqrstuvwxyz1234567890", "id_token": "abcdefghijklmnopqrstuvwxyz1234567890" } -
The client must generate the
auth.tomlfile with the above values. The following extra key/values must be added by the client to the auth.toml:expires_at: <expires_in> + <time()>This value is required to determine whether the token is expired and needs refresh. This is missing in the token response so it must be added by summing theexpires_invalue with the current timestamp.refresh_urlThis value is also missing in the device token response but is necessary for refreshing expired tokens. This field must be added with value same as thedevice_token_refresh_urlfrom the package server authentication configuration endpoint response.
The client_id parameter for device authentication can be configured by setting the environment variable JULIA_PKG_AUTHENTICATION_DEVICE_CLIENT_ID. This value defaults to "device".