Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,27 @@ It's recommended to create a separate account for the DUB registry GitHub authen

It's absolutely recommended to create a personal access token without any extra permissions for your GitHub account instead of entering your password plain text into the settings file. You can generate an access token at https://github.com/settings/tokens (Settings -> Developer Settings -> Personal access tokens)

Native app / CLI login (OAuth)
------------------------------

Package-management tools (for example a local `dub-publish` CLI) can log in with
the OAuth 2.0 authorization-code + PKCE flow and a loopback callback instead of
posting a password. See [api-docs/oauth.md](api-docs/oauth.md).

Optional GitHub login for the website (and thus for that authorize step) uses a
GitHub OAuth App. Add the app's credentials to `settings.json` and set the
callback URL to `https://<this-host>/login/github/callback`:

```json
{
"github-oauth-client-id": "<GitHub OAuth App client ID>",
"github-oauth-client-secret": "<GitHub OAuth App client secret>"
}
```

This is separate from `github-auth`, which is only a personal access token for
GitHub API rate limits when polling packages.

### SECURITY NOTICE

Development versions prior to 2.3.0 were leaking the GitLab private token in error messages shown to the user. Please make sure to use the latest version along with a freshly generated token.
Expand Down
88 changes: 88 additions & 0 deletions api-docs/oauth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Native app OAuth (authorization code + PKCE)

The registry issues access tokens to local CLI/GUI tools so they can manage
packages without posting a password. This is OAuth 2.0 authorization code with
PKCE (RFC 7636) and loopback redirects (RFC 8252).

Website login with GitHub is separate: `GET /login/github` → GitHub →
`GET /login/github/callback`. It is enabled only when `github-oauth-client-id`
and `github-oauth-client-secret` are set.

## Public clients

No client registration. `client_id` is an optional label (`native`,
`dub-publish`, `dubx`, …). Every client is public: PKCE `S256` is required, and
`redirect_uri` must be `http://127.0.0.1`, `http://localhost`, or `http://[::1]`
(any port and path).

Scope: `packages` (manage packages for the logged-in user).

## `GET /oauth/authorize`

Query:

| Param | Required | Notes |
| --- | --- | --- |
| `response_type` | yes | `code` |
| `client_id` | no | default `native` |
| `redirect_uri` | yes | loopback HTTP URI |
| `state` | recommended | echoed back |
| `code_challenge` | yes | S256 |
| `code_challenge_method` | yes | `S256` |
| `scope` | no | default `packages` |

If the browser has no session, the user is sent to `/login?redirect=…` (password
and, when configured, GitHub). After login they see a consent page. Authorize
redirects to `redirect_uri?code=…&state=…`. Deny uses `error=access_denied`.

Invalid `redirect_uri` is **not** redirected (error page instead).

## `POST /oauth/token`

`application/x-www-form-urlencoded` or `application/json`:

| Field | Required |
| --- | --- |
| `grant_type` | `authorization_code` |
| `code` | from the callback |
| `redirect_uri` | exact match of the authorize request |
| `code_verifier` | PKCE verifier |
| `client_id` | same as authorize |

Success:

```json
{
"access_token": "…",
"token_type": "Bearer",
"expires_in": 7776000,
"scope": "packages"
}
```

Authorization codes are single-use and expire in 5 minutes. Access tokens expire
in 90 days. Codes and tokens are stored as SHA-256 hashes.

Errors use OAuth JSON: `{"error":"invalid_grant","error_description":"…"}`.

## `POST /oauth/revoke`

Form/JSON field `token`. Always returns 200 if the request is well-formed.

## Calling authenticated routes

Send the token on existing owner routes (register package, my_packages, …):

```
Authorization: Bearer <access_token>
```

Session cookies still work for the browser.

## CLI sketch

1. Listen on `http://127.0.0.1:<ephemeral>/callback`.
2. Create a PKCE verifier and S256 challenge.
3. Open `/oauth/authorize?response_type=code&client_id=dub-publish&redirect_uri=…&state=…&code_challenge=…&code_challenge_method=S256`.
4. On the callback, `POST /oauth/token` with the code and verifier.
5. Store the access token; send `Authorization: Bearer` on later requests.
8 changes: 7 additions & 1 deletion source/app.d
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import dubregistry.registry;
import dubregistry.web;
import dubregistry.api;
import dubregistry.config;
import dubregistry.oauth;
import dubregistry.oauthstore;

import std.algorithm : sort;
import std.process : environment;
Expand Down Expand Up @@ -155,6 +157,7 @@ void main()
s_registry = new DubRegistry(regsettings);

UserManController userdb;
OAuthStore oauthStore;

if (!s_mirror.length) {
// user management
Expand Down Expand Up @@ -184,6 +187,7 @@ void main()
}

userdb = createUserManController(udbsettings);
oauthStore = new OAuthStore(databaseName);
}

if (noServe) {
Expand All @@ -193,7 +197,9 @@ void main()
}

// web front end
s_web = router.registerDubRegistryWebFrontend(s_registry, userdb);
if (userdb && oauthStore)
registerDubRegistryOAuth(router, userdb, oauthStore, appConfig);
s_web = router.registerDubRegistryWebFrontend(s_registry, userdb, oauthStore);
router.registerDubRegistryAPI(s_registry);

// check whether dummy data should be loaded
Expand Down
5 changes: 5 additions & 0 deletions source/dubregistry/config.d
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ public struct AppConfig
{
@Name("github-auth") @Optional
string ghauth;
@Name("github-oauth-client-id") @Optional
string ghoauthid;
@Name("github-oauth-client-secret") @Optional
string ghoauthsecret;
@Name("gitlab-url") @Optional
string glurl;
@Name("gitlab-auth") @Optional
Expand Down Expand Up @@ -116,6 +120,7 @@ unittest
`;
auto conf = AppConfig.readString(str);
assert(conf.ghauth == "foo");
assert(conf.ghoauthid.length == 0);
assert(conf.glauth.length == 0);
assert(conf.enforceCertificateTrust == true);
assert(conf.mailConnectionType == SMTPConnectionType.startTLS);
Expand Down
Loading