Skip to content

Commit 8cf590f

Browse files
committed
feat: add support for GitHub enterprise
1 parent a8f3057 commit 8cf590f

File tree

7 files changed

+139
-29
lines changed

7 files changed

+139
-29
lines changed

README.md

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ to get those tokens in the right place.
99
## Features
1010

1111
- OAuth device flow authentication (no manual token creation needed)
12-
- Support for multiple providers (GitHub and GitLab)
12+
- Support for multiple providers (GitHub, GitHub Enterprise, and GitLab)
1313
- Secure token storage in `~/.config/nix/nix.conf`
1414
- Token validation and status checking
1515
- Automatic backup creation before modifying configuration
@@ -73,12 +73,28 @@ Authenticate with GitLab:
7373
nix-auth login gitlab
7474
```
7575

76+
Authenticate with GitHub Enterprise or GitLab self-hosted:
77+
78+
```bash
79+
# GitHub Enterprise
80+
nix-auth login github --host github.company.com --client-id <your-client-id>
81+
82+
# GitLab self-hosted
83+
nix-auth login gitlab --host gitlab.company.com --client-id <your-application-id>
84+
```
85+
7686
The tool will:
7787
1. Display a one-time code
7888
2. Open your browser to the provider's device authorization page
7989
3. Wait for you to authorize the application
8090
4. Save the token to `~/.config/nix/nix.conf`
8191

92+
**Note for self-hosted instances**:
93+
- **GitHub Enterprise**: You'll need to create an OAuth App and provide the client ID via `--client-id`
94+
- **GitLab self-hosted**: You'll need to create an OAuth application and provide the client ID via `--client-id`
95+
96+
The tool will guide you through this process if the client ID is not provided. You can also set the `GITHUB_CLIENT_ID` or `GITLAB_CLIENT_ID` environment variables as an alternative to the `--client-id` flag.
97+
8298
### Check Status
8399

84100
View all configured tokens:
@@ -95,19 +111,25 @@ Remove a token interactively:
95111
nix-auth logout
96112
```
97113

98-
Or remove a specific provider's token:
114+
Remove a specific provider's token:
99115

100116
```bash
101117
nix-auth logout github
102118
```
103119

120+
Remove a token for a specific host:
121+
122+
```bash
123+
nix-auth logout --host github.company.com
124+
```
125+
104126
## How It Works
105127

106128
The tool manages the `access-tokens` configuration in your `~/.config/nix/nix.conf` file. This allows Nix to authenticate when fetching flake inputs from private repositories or builtins fetchers, and hitting rate limits.
107129

108130
Example configuration added by this tool:
109131
```
110-
access-tokens = github.com=ghp_xxxxxxxxxxxxxxxxxxxx gitlab.com=glpat-xxxxxxxxxxxx
132+
access-tokens = github.com=ghp_xxxxxxxxxxxxxxxxxxxx gitlab.com=glpat-xxxxxxxxxxxx github.company.com=ghp_yyyyyyyy
111133
```
112134

113135
## Security
@@ -119,7 +141,7 @@ access-tokens = github.com=ghp_xxxxxxxxxxxxxxxxxxxx gitlab.com=glpat-xxxxxxxxxxx
119141

120142
## Future Plans
121143

122-
- Support for more providers; Gitea / Forgego / GitHub Enterprise / ...
144+
- Support for more providers (Gitea, Forgejo, Bitbucket, etc.)
123145
- Token expiration notifications
124146
- Integration with system keychains for secure storage (will require patching
125147
Nix)

cmd/login.go

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cmd
33
import (
44
"context"
55
"fmt"
6+
"os"
67
"strings"
78

89
"github.com/numtide/nix-auth/internal/config"
@@ -16,19 +17,22 @@ var loginCmd = &cobra.Command{
1617
Short: "Authenticate with a provider and save the access token",
1718
Long: `Authenticate with a provider (GitHub, GitLab, etc.) using OAuth device flow
1819
and save the access token to your nix.conf for use with Nix flakes.`,
19-
Example: ` nix-auth login # defaults to GitHub
20+
Example: ` nix-auth login # defaults to GitHub
2021
nix-auth login github
21-
nix-auth login gitlab`,
22+
nix-auth login gitlab
23+
nix-auth login github --host github.company.com --client-id abc123`,
2224
Args: cobra.MaximumNArgs(1),
2325
RunE: runLogin,
2426
}
2527

2628
var (
27-
loginHost string
29+
loginHost string
30+
loginClientID string
2831
)
2932

3033
func init() {
3134
loginCmd.Flags().StringVar(&loginHost, "host", "", "Custom host (e.g., github.company.com)")
35+
loginCmd.Flags().StringVar(&loginClientID, "client-id", "", "OAuth client ID (required for self-hosted instances)")
3236
}
3337

3438
func runLogin(cmd *cobra.Command, args []string) error {
@@ -48,11 +52,25 @@ func runLogin(cmd *cobra.Command, args []string) error {
4852
host := prov.Host()
4953
if loginHost != "" {
5054
host = loginHost
51-
// Set custom host for GitLab provider
52-
if gitlabProv, ok := prov.(*provider.GitLabProvider); ok {
53-
gitlabProv.SetHost(host)
55+
}
56+
57+
// Always set the host (even if it's the default)
58+
prov.SetHost(host)
59+
60+
// Set client ID: use flag, fallback to environment variable
61+
clientID := loginClientID
62+
if clientID == "" {
63+
// Check provider-specific environment variable
64+
switch providerName {
65+
case "github":
66+
clientID = os.Getenv("GITHUB_CLIENT_ID")
67+
case "gitlab":
68+
clientID = os.Getenv("GITLAB_CLIENT_ID")
5469
}
5570
}
71+
if clientID != "" {
72+
prov.SetClientID(clientID)
73+
}
5674

5775
fmt.Printf("Authenticating with %s (%s)...\n", prov.Name(), host)
5876

internal/provider/device_flow.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,3 @@ func ShowWaitingMessage() {
3232
fmt.Println()
3333
fmt.Println("Waiting for authorization...")
3434
}
35-

internal/provider/github.go

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,55 @@ func init() {
1414
Register("github", &GitHubProvider{})
1515
}
1616

17-
type GitHubProvider struct{}
17+
type GitHubProvider struct {
18+
host string
19+
clientID string
20+
}
21+
22+
// SetHost sets a custom host for the GitHub provider
23+
func (g *GitHubProvider) SetHost(host string) {
24+
g.host = host
25+
}
26+
27+
// SetClientID sets a custom OAuth client ID for the GitHub provider
28+
func (g *GitHubProvider) SetClientID(clientID string) {
29+
g.clientID = clientID
30+
}
31+
32+
// getBaseURL returns the base URL for web URLs
33+
func (g *GitHubProvider) getBaseURL() string {
34+
if g.host != "" && g.host != "github.com" {
35+
return fmt.Sprintf("https://%s", g.host)
36+
}
37+
return "https://github.com"
38+
}
39+
40+
// getAPIURL returns the base URL for API calls
41+
func (g *GitHubProvider) getAPIURL() string {
42+
if g.host != "" && g.host != "github.com" {
43+
// GitHub Enterprise uses {host}/api/v3
44+
return fmt.Sprintf("https://%s/api/v3", g.host)
45+
}
46+
// GitHub.com uses api.github.com
47+
return "https://api.github.com"
48+
}
1849

1950
// makeGitHubAPIRequest is a helper function to make authenticated requests to GitHub API
2051
func (g *GitHubProvider) makeGitHubAPIRequest(ctx context.Context, token string, endpoint string) (*http.Response, error) {
2152
headers := map[string]string{
2253
"Accept": "application/vnd.github.v3+json",
2354
}
24-
return makeAuthenticatedRequest(ctx, "GET", endpoint, token, "token "+token, headers)
55+
return makeAuthenticatedRequest(ctx, "GET", endpoint, "token "+token, headers)
2556
}
2657

2758
func (g *GitHubProvider) Name() string {
2859
return "github"
2960
}
3061

3162
func (g *GitHubProvider) Host() string {
63+
if g.host != "" {
64+
return g.host
65+
}
3266
return "github.com"
3367
}
3468

@@ -38,13 +72,35 @@ func (g *GitHubProvider) GetScopes() []string {
3872
}
3973

4074
func (g *GitHubProvider) Authenticate(ctx context.Context) (string, error) {
41-
clientID := "178c6fc778ccc68e1d6a" // GitHub CLI's client ID - widely used for CLI tools
42-
scopes := g.GetScopes()
75+
clientID := g.clientID
76+
if clientID == "" {
77+
if g.host == "github.com" || g.host == "" {
78+
clientID = "178c6fc778ccc68e1d6a" // GitHub CLI's client ID - widely used for CLI tools
79+
} else {
80+
// Provide instructions for creating an OAuth app
81+
fmt.Println("GitHub Enterprise OAuth authentication requires a Client ID.")
82+
fmt.Println("\nTo create one:")
83+
fmt.Printf("1. Go to %s/settings/applications/new\n", g.getBaseURL())
84+
fmt.Println("2. Create a new OAuth App with:")
85+
fmt.Println(" - Application name: nix-auth (or any name you prefer)")
86+
fmt.Println(" - Homepage URL: https://github.com/numtide/nix-auth")
87+
fmt.Println(" - Authorization callback URL: http://127.0.0.1/callback")
88+
fmt.Println("3. After creating, copy the Client ID")
89+
fmt.Println("\nThen run:")
90+
fmt.Printf(" nix-auth login github --host %s --client-id <your-client-id>\n", g.host)
91+
fmt.Println("\nOr set the GITHUB_CLIENT_ID environment variable:")
92+
fmt.Println(" export GITHUB_CLIENT_ID=<your-client-id>")
93+
fmt.Printf(" nix-auth login github --host %s\n", g.host)
94+
return "", fmt.Errorf("client ID required for GitHub Enterprise (use --client-id flag or GITHUB_CLIENT_ID env var)")
95+
}
96+
}
4397

98+
scopes := g.GetScopes()
4499
httpClient := &http.Client{}
45100

46101
// Request device code
47-
code, err := device.RequestCode(httpClient, "https://github.com/login/device/code", clientID, scopes)
102+
deviceCodeURL := fmt.Sprintf("%s/login/device/code", g.getBaseURL())
103+
code, err := device.RequestCode(httpClient, deviceCodeURL, clientID, scopes)
48104
if err != nil {
49105
return "", fmt.Errorf("failed to request device code: %w", err)
50106
}
@@ -54,7 +110,8 @@ func (g *GitHubProvider) Authenticate(ctx context.Context) (string, error) {
54110
ShowWaitingMessage()
55111

56112
// Wait for user to authorize
57-
accessToken, err := device.Wait(ctx, httpClient, "https://github.com/login/oauth/access_token", device.WaitOptions{
113+
accessTokenURL := fmt.Sprintf("%s/login/oauth/access_token", g.getBaseURL())
114+
accessToken, err := device.Wait(ctx, httpClient, accessTokenURL, device.WaitOptions{
58115
ClientID: clientID,
59116
DeviceCode: code,
60117
})
@@ -66,7 +123,8 @@ func (g *GitHubProvider) Authenticate(ctx context.Context) (string, error) {
66123
}
67124

68125
func (g *GitHubProvider) ValidateToken(ctx context.Context, token string) error {
69-
resp, err := g.makeGitHubAPIRequest(ctx, token, "https://api.github.com/user")
126+
userURL := fmt.Sprintf("%s/user", g.getAPIURL())
127+
resp, err := g.makeGitHubAPIRequest(ctx, token, userURL)
70128
if err != nil {
71129
return fmt.Errorf("failed to validate token: %w", err)
72130
}
@@ -76,7 +134,8 @@ func (g *GitHubProvider) ValidateToken(ctx context.Context, token string) error
76134
}
77135

78136
func (g *GitHubProvider) GetUserInfo(ctx context.Context, token string) (username, fullName string, err error) {
79-
resp, err := g.makeGitHubAPIRequest(ctx, token, "https://api.github.com/user")
137+
userURL := fmt.Sprintf("%s/user", g.getAPIURL())
138+
resp, err := g.makeGitHubAPIRequest(ctx, token, userURL)
80139
if err != nil {
81140
return "", "", fmt.Errorf("failed to get user info: %w", err)
82141
}
@@ -95,7 +154,8 @@ func (g *GitHubProvider) GetUserInfo(ctx context.Context, token string) (usernam
95154
}
96155

97156
func (g *GitHubProvider) GetTokenScopes(ctx context.Context, token string) ([]string, error) {
98-
resp, err := g.makeGitHubAPIRequest(ctx, token, "https://api.github.com/user")
157+
userURL := fmt.Sprintf("%s/user", g.getAPIURL())
158+
resp, err := g.makeGitHubAPIRequest(ctx, token, userURL)
99159
if err != nil {
100160
return nil, fmt.Errorf("failed to check token scopes: %w", err)
101161
}

internal/provider/gitlab.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"fmt"
77
"net/http"
88
"net/url"
9-
"os"
109
"strings"
1110
"time"
1211
)
@@ -16,14 +15,20 @@ func init() {
1615
}
1716

1817
type GitLabProvider struct {
19-
host string
18+
host string
19+
clientID string
2020
}
2121

2222
// SetHost sets a custom host for the GitLab provider
2323
func (g *GitLabProvider) SetHost(host string) {
2424
g.host = host
2525
}
2626

27+
// SetClientID sets a custom OAuth client ID for the GitLab provider
28+
func (g *GitLabProvider) SetClientID(clientID string) {
29+
g.clientID = clientID
30+
}
31+
2732
// getBaseURL returns the base URL for API calls
2833
func (g *GitLabProvider) getBaseURL() string {
2934
if g.host != "" && g.host != "gitlab.com" {
@@ -59,7 +64,7 @@ func (g *GitLabProvider) makeGitLabAPIRequest(ctx context.Context, token string,
5964
headers := map[string]string{
6065
"Accept": "application/json",
6166
}
62-
return makeAuthenticatedRequest(ctx, "GET", endpoint, token, "Bearer "+token, headers)
67+
return makeAuthenticatedRequest(ctx, "GET", endpoint, "Bearer "+token, headers)
6368
}
6469

6570
func (g *GitLabProvider) Name() string {
@@ -79,8 +84,7 @@ func (g *GitLabProvider) GetScopes() []string {
7984
}
8085

8186
func (g *GitLabProvider) Authenticate(ctx context.Context) (string, error) {
82-
// Check for GitLab client ID in environment
83-
clientID := os.Getenv("GITLAB_CLIENT_ID")
87+
clientID := g.clientID
8488
if clientID == "" {
8589
if g.host == "gitlab.com" || g.host == "" {
8690
// FIXME: taken from https://gitlab.com/gitlab-org/cli/-/issues/1338
@@ -97,9 +101,11 @@ func (g *GitLabProvider) Authenticate(ctx context.Context) (string, error) {
97101
fmt.Println(" - Scopes: ☑ read_api")
98102
fmt.Println("3. Copy the Application ID")
99103
fmt.Println("\nThen run:")
104+
fmt.Printf(" nix-auth login gitlab --host %s --client-id <your-application-id>\n", g.host)
105+
fmt.Println("\nOr set the GITLAB_CLIENT_ID environment variable:")
100106
fmt.Println(" export GITLAB_CLIENT_ID=<your-application-id>")
101-
fmt.Println(" nix-auth login gitlab")
102-
return "", fmt.Errorf("GITLAB_CLIENT_ID environment variable not set")
107+
fmt.Printf(" nix-auth login gitlab --host %s\n", g.host)
108+
return "", fmt.Errorf("client ID required for GitLab self-hosted (use --client-id flag or GITLAB_CLIENT_ID env var)")
103109
}
104110
}
105111

internal/provider/http_client.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88

99
// makeAuthenticatedRequest creates and executes an authenticated HTTP request
1010
// with common error handling for authentication providers
11-
func makeAuthenticatedRequest(ctx context.Context, method, url, token, authHeader string, headers map[string]string) (*http.Response, error) {
11+
func makeAuthenticatedRequest(ctx context.Context, method, url, authHeader string, headers map[string]string) (*http.Response, error) {
1212
req, err := http.NewRequestWithContext(ctx, method, url, nil)
1313
if err != nil {
1414
return nil, err
@@ -40,4 +40,3 @@ func makeAuthenticatedRequest(ctx context.Context, method, url, token, authHeade
4040
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
4141
}
4242
}
43-

internal/provider/provider.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ type Provider interface {
1010
// Host returns the default host for this provider
1111
Host() string
1212

13+
// SetHost sets a custom host for this provider
14+
SetHost(host string)
15+
16+
// SetClientID sets a custom OAuth client ID for this provider
17+
SetClientID(clientID string)
18+
1319
// Authenticate performs the OAuth flow and returns an access token
1420
Authenticate(ctx context.Context) (string, error)
1521

0 commit comments

Comments
 (0)