Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .deadcode-baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ internal/googleauth/oauth_flow_manual_redirect.go:54:6: unreachable func: extrac
internal/googleauth/scopes.go:34:6: unreachable func: ScopesForCommands
internal/googleauth/scopes.go:72:6: unreachable func: AllScopes
internal/googleauth/scopes.go:96:6: unreachable func: knownCommandNames
internal/msauth/broker.go:37:6: unreachable func: CreateBrokerSession
internal/msauth/broker.go:91:6: unreachable func: parseHTTPSURL
internal/msauth/broker_store.go:28:6: unreachable func: NewMemoryBrokerStore
internal/msauth/broker_store.go:32:29: unreachable func: MemoryBrokerStore.Save
internal/msauth/broker_store.go:50:29: unreachable func: MemoryBrokerStore.Consume
Expand Down
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ SHELL := /bin/bash
.DEFAULT_GOAL := build

.PHONY: build build-gog wk workit gog wk-help workit-help help fmt fmt-check lint lint-full test ci tools
.PHONY: worker-ci build-internal build-automagik deadcode race coverage
.PHONY: worker-ci build-internal build-automagik build-auth-server docker-auth-server deadcode race coverage

BIN_DIR := $(CURDIR)/bin
BIN := $(BIN_DIR)/wk
Expand Down Expand Up @@ -49,6 +49,13 @@ build:
@mkdir -p $(BIN_DIR)
@go build -ldflags "$(LDFLAGS)" -o $(BIN) $(CMD)

build-auth-server:
@mkdir -p $(BIN_DIR)
@cd auth-server && go build -o $(BIN_DIR)/workit-auth-server .

docker-auth-server:
@docker build -t workit-auth-server:local auth-server

# Build the deprecated "gog" backward-compat alias binary.
build-gog:
@mkdir -p $(BIN_DIR)
Expand Down
43 changes: 14 additions & 29 deletions auth-server/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,39 +1,24 @@
# Build stage
FROM golang:1.25-alpine AS builder
# syntax=docker/dockerfile:1.7

WORKDIR /app
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS build
ARG TARGETARCH
WORKDIR /src

# Copy go.mod and go.sum first for better caching
COPY go.mod go.sum ./
RUN go mod download

# Copy source code
COPY *.go ./

# Build the binary
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o auth-server .

# Runtime stage
FROM alpine:3.19

# Add ca-certificates for HTTPS calls to Google OAuth
RUN apk --no-cache add ca-certificates
COPY . ./
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w" -o /out/workit-auth-server .

FROM gcr.io/distroless/static-debian12:nonroot
WORKDIR /app
COPY --from=build /out/workit-auth-server /app/workit-auth-server

# Copy the binary from builder
COPY --from=builder /app/auth-server .

# Create non-root user
RUN adduser -D -u 1000 appuser
USER appuser

# Expose default port
ENV WK_PUBLIC_BASE_URL=""
ENV WK_M365_CLIENT_ID=""
ENV WK_M365_TENANT_ID="organizations"
ENV WK_M365_BROKER_TOKEN=""
EXPOSE 8080

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1

# Run the server
ENTRYPOINT ["./auth-server"]
USER nonroot:nonroot
ENTRYPOINT ["/app/workit-auth-server"]
32 changes: 20 additions & 12 deletions auth-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ When users authenticate via the headless OAuth flow:
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/health` | GET | Health check, returns `{"status": "ok"}` |
| `/callback` | GET | OAuth callback, exchanges code for token |
| `/callback` | GET | Google OAuth callback, exchanges code for token |
| `/token/{state}` | GET | Retrieve token (consumes it) |
| `/status/{state}` | GET | Check token status without consuming |
| `/m365/sessions` | POST | Create a one-click Microsoft 365 read-only login session |
| `/m365/start/{state}` | GET | Redirect user to Microsoft authorize URL |
| `/m365/callback` | GET | Microsoft OAuth callback with PKCE/email validation |

### Response Codes

Expand Down Expand Up @@ -51,7 +54,12 @@ When users authenticate via the headless OAuth flow:
|----------|-------------|
| `WK_CLIENT_ID` | OAuth client ID |
| `WK_CLIENT_SECRET` | OAuth client secret |
| `WK_REDIRECT_URL` | OAuth redirect URL |
| `WK_REDIRECT_URL` | Google OAuth redirect URL |
| `WK_PUBLIC_BASE_URL` | Public HTTPS base URL for the deployed auth server; M365 derives `/m365/callback` from this |
| `WK_CALLBACK_SERVER` | Backward-compatible public base URL fallback |
| `WK_M365_CLIENT_ID` | Microsoft Entra application/client ID for M365 broker |
| `WK_M365_TENANT_ID` | Microsoft tenant ID; defaults to `organizations` |
| `WK_M365_BROKER_TOKEN` | Required bearer token for trusted callers creating `/m365/sessions` |

Command-line flags take precedence over environment variables.

Expand All @@ -68,14 +76,17 @@ go build -o auth-server .
### Docker Build

```bash
docker build -t auth-server .
docker build -t workit-auth-server .
docker run -p 8080:8080 \
-e WK_CLIENT_ID="your-client-id" \
-e WK_CLIENT_SECRET="your-client-secret" \
-e WK_REDIRECT_URL="https://auth.example.com/callback" \
auth-server
-e WK_PUBLIC_BASE_URL="https://auth.hv.example" \
-e WK_M365_CLIENT_ID="<hapvida-entra-app-client-id>" \
-e WK_M365_TENANT_ID="<hapvida-tenant-id>" \
-e WK_M365_BROKER_TOKEN="<strong-random-admin-token>" \
workit-auth-server
```

For Google relay compatibility, also set `WK_CLIENT_ID`, `WK_CLIENT_SECRET`, and optionally `WK_REDIRECT_URL`.

## Deployment

### Docker Compose Example
Expand All @@ -92,13 +103,10 @@ services:
- WK_CLIENT_SECRET=${WK_CLIENT_SECRET}
- WK_REDIRECT_URL=https://auth.example.com/callback
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/health"]
interval: 30s
timeout: 3s
retries: 3
```

The runtime image is distroless and has no shell utilities such as `wget`; configure Kubernetes/OCI HTTP probes against `/health` instead of a container-local shell healthcheck.

### Reverse Proxy (nginx)

```nginx
Expand Down
61 changes: 61 additions & 0 deletions auth-server/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package main

import (
"fmt"
"os"
"strings"
)

type serverConfigInput struct {
Port int
ClientID string
ClientSecret string
RedirectURL string
PublicBaseURL string
M365ClientID string
M365TenantID string
M365AdminToken string
}

type serverConfig struct {
clientID string
clientSecret string
redirectURL string
publicBaseURL string
m365ClientID string
m365TenantID string
m365AdminToken string
}

func resolveServerConfig(input serverConfigInput) serverConfig {
publicBaseURL := firstNonEmpty(input.PublicBaseURL, os.Getenv("WK_PUBLIC_BASE_URL"), os.Getenv("WK_CALLBACK_SERVER"))
publicBaseURL = strings.TrimRight(strings.TrimSpace(publicBaseURL), "/")

redirectURL := firstNonEmpty(input.RedirectURL, os.Getenv("WK_REDIRECT_URL"))
if redirectURL == "" && publicBaseURL != "" {
redirectURL = publicBaseURL + "/callback"
}
if redirectURL == "" {
redirectURL = fmt.Sprintf("http://localhost:%d/callback", input.Port)
}

return serverConfig{
clientID: firstNonEmpty(input.ClientID, os.Getenv("WK_CLIENT_ID")),
clientSecret: firstNonEmpty(input.ClientSecret, os.Getenv("WK_CLIENT_SECRET")),
redirectURL: redirectURL,
publicBaseURL: publicBaseURL,
m365ClientID: firstNonEmpty(input.M365ClientID, os.Getenv("WK_M365_CLIENT_ID")),
m365TenantID: firstNonEmpty(input.M365TenantID, os.Getenv("WK_M365_TENANT_ID")),
m365AdminToken: firstNonEmpty(input.M365AdminToken, os.Getenv("WK_M365_BROKER_TOKEN"), os.Getenv("WK_BROKER_ADMIN_TOKEN")),
}
}

func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}

return ""
}
21 changes: 21 additions & 0 deletions auth-server/dockerfile_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package main

import (
"os"
"strings"
"testing"
)

func TestDockerfileDocumentsEnterpriseM365EnvContract(t *testing.T) {
data, err := os.ReadFile("Dockerfile")
if err != nil {
t.Fatalf("read Dockerfile: %v", err)
}

content := string(data)
for _, want := range []string{"WK_PUBLIC_BASE_URL", "WK_M365_CLIENT_ID", "WK_M365_TENANT_ID", "WK_M365_BROKER_TOKEN", "TARGETARCH", "EXPOSE 8080", "workit-auth-server"} {
if !strings.Contains(content, want) {
t.Fatalf("Dockerfile missing %s", want)
}
}
}
7 changes: 7 additions & 0 deletions auth-server/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ type Server struct {
oauthConfig *oauth2.Config
mux *http.ServeMux
exchangeFunc func(ctx context.Context, code string) (*oauth2.Token, error)

m365Enabled bool
m365ClientID string
m365TenantID string
m365AdminToken string
publicBaseURL string
m365Sessions *m365SessionStore
}

// NewServer creates a new Server with the given configuration.
Expand Down
Loading
Loading