Skip to content

Commit 9ed729e

Browse files
feat: wire M365 broker into auth server (#47)
* feat: wire m365 broker into auth server * fix: harden m365 auth server broker * fix: secure m365 broker session creation --------- Co-authored-by: Genie Automagik <genie@namastex.ai>
1 parent da942b5 commit 9ed729e

13 files changed

Lines changed: 1020 additions & 87 deletions

.deadcode-baseline.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ internal/googleauth/oauth_flow_manual_redirect.go:54:6: unreachable func: extrac
3939
internal/googleauth/scopes.go:34:6: unreachable func: ScopesForCommands
4040
internal/googleauth/scopes.go:72:6: unreachable func: AllScopes
4141
internal/googleauth/scopes.go:96:6: unreachable func: knownCommandNames
42+
internal/msauth/broker.go:37:6: unreachable func: CreateBrokerSession
43+
internal/msauth/broker.go:91:6: unreachable func: parseHTTPSURL
4244
internal/msauth/broker_store.go:28:6: unreachable func: NewMemoryBrokerStore
4345
internal/msauth/broker_store.go:32:29: unreachable func: MemoryBrokerStore.Save
4446
internal/msauth/broker_store.go:50:29: unreachable func: MemoryBrokerStore.Consume

Makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ SHELL := /bin/bash
44
.DEFAULT_GOAL := build
55

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

99
BIN_DIR := $(CURDIR)/bin
1010
BIN := $(BIN_DIR)/wk
@@ -49,6 +49,13 @@ build:
4949
@mkdir -p $(BIN_DIR)
5050
@go build -ldflags "$(LDFLAGS)" -o $(BIN) $(CMD)
5151

52+
build-auth-server:
53+
@mkdir -p $(BIN_DIR)
54+
@cd auth-server && go build -o $(BIN_DIR)/workit-auth-server .
55+
56+
docker-auth-server:
57+
@docker build -t workit-auth-server:local auth-server
58+
5259
# Build the deprecated "gog" backward-compat alias binary.
5360
build-gog:
5461
@mkdir -p $(BIN_DIR)

auth-server/Dockerfile

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,24 @@
1-
# Build stage
2-
FROM golang:1.25-alpine AS builder
1+
# syntax=docker/dockerfile:1.7
32

4-
WORKDIR /app
3+
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS build
4+
ARG TARGETARCH
5+
WORKDIR /src
56

6-
# Copy go.mod and go.sum first for better caching
77
COPY go.mod go.sum ./
88
RUN go mod download
99

10-
# Copy source code
11-
COPY *.go ./
12-
13-
# Build the binary
14-
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o auth-server .
15-
16-
# Runtime stage
17-
FROM alpine:3.19
18-
19-
# Add ca-certificates for HTTPS calls to Google OAuth
20-
RUN apk --no-cache add ca-certificates
10+
COPY . ./
11+
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w" -o /out/workit-auth-server .
2112

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

24-
# Copy the binary from builder
25-
COPY --from=builder /app/auth-server .
26-
27-
# Create non-root user
28-
RUN adduser -D -u 1000 appuser
29-
USER appuser
30-
31-
# Expose default port
17+
ENV WK_PUBLIC_BASE_URL=""
18+
ENV WK_M365_CLIENT_ID=""
19+
ENV WK_M365_TENANT_ID="organizations"
20+
ENV WK_M365_BROKER_TOKEN=""
3221
EXPOSE 8080
3322

34-
# Health check
35-
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
36-
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
37-
38-
# Run the server
39-
ENTRYPOINT ["./auth-server"]
23+
USER nonroot:nonroot
24+
ENTRYPOINT ["/app/workit-auth-server"]

auth-server/README.md

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@ When users authenticate via the headless OAuth flow:
1717
| Endpoint | Method | Description |
1818
|----------|--------|-------------|
1919
| `/health` | GET | Health check, returns `{"status": "ok"}` |
20-
| `/callback` | GET | OAuth callback, exchanges code for token |
20+
| `/callback` | GET | Google OAuth callback, exchanges code for token |
2121
| `/token/{state}` | GET | Retrieve token (consumes it) |
2222
| `/status/{state}` | GET | Check token status without consuming |
23+
| `/m365/sessions` | POST | Create a one-click Microsoft 365 read-only login session |
24+
| `/m365/start/{state}` | GET | Redirect user to Microsoft authorize URL |
25+
| `/m365/callback` | GET | Microsoft OAuth callback with PKCE/email validation |
2326

2427
### Response Codes
2528

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

5664
Command-line flags take precedence over environment variables.
5765

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

7078
```bash
71-
docker build -t auth-server .
79+
docker build -t workit-auth-server .
7280
docker run -p 8080:8080 \
73-
-e WK_CLIENT_ID="your-client-id" \
74-
-e WK_CLIENT_SECRET="your-client-secret" \
75-
-e WK_REDIRECT_URL="https://auth.example.com/callback" \
76-
auth-server
81+
-e WK_PUBLIC_BASE_URL="https://auth.hv.example" \
82+
-e WK_M365_CLIENT_ID="<hapvida-entra-app-client-id>" \
83+
-e WK_M365_TENANT_ID="<hapvida-tenant-id>" \
84+
-e WK_M365_BROKER_TOKEN="<strong-random-admin-token>" \
85+
workit-auth-server
7786
```
7887

88+
For Google relay compatibility, also set `WK_CLIENT_ID`, `WK_CLIENT_SECRET`, and optionally `WK_REDIRECT_URL`.
89+
7990
## Deployment
8091

8192
### Docker Compose Example
@@ -92,13 +103,10 @@ services:
92103
- WK_CLIENT_SECRET=${WK_CLIENT_SECRET}
93104
- WK_REDIRECT_URL=https://auth.example.com/callback
94105
restart: unless-stopped
95-
healthcheck:
96-
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/health"]
97-
interval: 30s
98-
timeout: 3s
99-
retries: 3
100106
```
101107
108+
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.
109+
102110
### Reverse Proxy (nginx)
103111

104112
```nginx

auth-server/config.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
)
8+
9+
type serverConfigInput struct {
10+
Port int
11+
ClientID string
12+
ClientSecret string
13+
RedirectURL string
14+
PublicBaseURL string
15+
M365ClientID string
16+
M365TenantID string
17+
M365AdminToken string
18+
}
19+
20+
type serverConfig struct {
21+
clientID string
22+
clientSecret string
23+
redirectURL string
24+
publicBaseURL string
25+
m365ClientID string
26+
m365TenantID string
27+
m365AdminToken string
28+
}
29+
30+
func resolveServerConfig(input serverConfigInput) serverConfig {
31+
publicBaseURL := firstNonEmpty(input.PublicBaseURL, os.Getenv("WK_PUBLIC_BASE_URL"), os.Getenv("WK_CALLBACK_SERVER"))
32+
publicBaseURL = strings.TrimRight(strings.TrimSpace(publicBaseURL), "/")
33+
34+
redirectURL := firstNonEmpty(input.RedirectURL, os.Getenv("WK_REDIRECT_URL"))
35+
if redirectURL == "" && publicBaseURL != "" {
36+
redirectURL = publicBaseURL + "/callback"
37+
}
38+
if redirectURL == "" {
39+
redirectURL = fmt.Sprintf("http://localhost:%d/callback", input.Port)
40+
}
41+
42+
return serverConfig{
43+
clientID: firstNonEmpty(input.ClientID, os.Getenv("WK_CLIENT_ID")),
44+
clientSecret: firstNonEmpty(input.ClientSecret, os.Getenv("WK_CLIENT_SECRET")),
45+
redirectURL: redirectURL,
46+
publicBaseURL: publicBaseURL,
47+
m365ClientID: firstNonEmpty(input.M365ClientID, os.Getenv("WK_M365_CLIENT_ID")),
48+
m365TenantID: firstNonEmpty(input.M365TenantID, os.Getenv("WK_M365_TENANT_ID")),
49+
m365AdminToken: firstNonEmpty(input.M365AdminToken, os.Getenv("WK_M365_BROKER_TOKEN"), os.Getenv("WK_BROKER_ADMIN_TOKEN")),
50+
}
51+
}
52+
53+
func firstNonEmpty(values ...string) string {
54+
for _, value := range values {
55+
if strings.TrimSpace(value) != "" {
56+
return strings.TrimSpace(value)
57+
}
58+
}
59+
60+
return ""
61+
}

auth-server/dockerfile_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package main
2+
3+
import (
4+
"os"
5+
"strings"
6+
"testing"
7+
)
8+
9+
func TestDockerfileDocumentsEnterpriseM365EnvContract(t *testing.T) {
10+
data, err := os.ReadFile("Dockerfile")
11+
if err != nil {
12+
t.Fatalf("read Dockerfile: %v", err)
13+
}
14+
15+
content := string(data)
16+
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"} {
17+
if !strings.Contains(content, want) {
18+
t.Fatalf("Dockerfile missing %s", want)
19+
}
20+
}
21+
}

auth-server/handlers.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ type Server struct {
1919
oauthConfig *oauth2.Config
2020
mux *http.ServeMux
2121
exchangeFunc func(ctx context.Context, code string) (*oauth2.Token, error)
22+
23+
m365Enabled bool
24+
m365ClientID string
25+
m365TenantID string
26+
m365AdminToken string
27+
publicBaseURL string
28+
m365Sessions *m365SessionStore
2229
}
2330

2431
// NewServer creates a new Server with the given configuration.

0 commit comments

Comments
 (0)