diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76bc91f..c6fced7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,11 +1,8 @@ name: Ci - on: pull_request: branches: [ main, master, development ] - jobs: - lint: name: Lint go code with golangci runs-on: ubuntu-latest @@ -21,7 +18,25 @@ jobs: ls -la make lint shell: bash - + + build: + name: Build project + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.23.4' + - name: Make repo safe + run: git config --global --add safe.directory /__w/gauth/gauth + - name: Build + run: | + make build-examples + shell: bash test: name: Run ci-tests @@ -34,7 +49,7 @@ jobs: - name: setup Go uses: actions/setup-go@v5 with: - go-version: '1.22.2' # + go-version: '1.23.4' # - name: Make repo safe run: git config --global --add safe.directory /__w/gauth/gauth - name: Run tests diff --git a/Makefile b/Makefile index 0a5c56b..6f2bfa8 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,50 @@ -.PHONY: test -test: - go test ./... -v -lint: - GOFLAGS=-buildvcs=false golangci-lint run --timeout 5m0s -v .DEFAULT_GOAL := dev + +.PHONY: build-basic +build-basic: + cd examples/basic && go build ./... + +.PHONY: build-validation +build-validation: + cd examples/validation && go build ./... + +.PHONY: build-m2m +build-m2m: + cd examples/m2m && go build ./... + +.PHONY: test-basic +test-basic: + cd examples/basic && go test ./... -v + +.PHONY: test-validation +test-validation: + cd examples/validation && go test ./... -v + +.PHONY: test-m2m +test-m2m: + cd examples/m2m && go test ./... -v + +.PHONY: lint-basic +lint-basic: + cd examples/basic && GOFLAGS=-buildvcs=false golangci-lint run --timeout 5m0s -v + +.PHONY: lint-validation +lint-validation: + cd examples/validation && GOFLAGS=-buildvcs=false golangci-lint run --timeout 5m0s -v + +.PHONY: lint-m2m +lint-m2m: + cd examples/m2m && GOFLAGS=-buildvcs=false golangci-lint run --timeout 5m0s -v + +.PHONY: build-examples +build-examples: build-basic build-validation build-m2m + +.PHONY: test +test: test-basic test-validation test-m2m + +.PHONY: lint +lint: lint-basic lint-validation lint-m2m + +.PHONY: dev +dev: + @echo "Development target (customize as needed)" diff --git a/examples/basic/main.go b/examples/basic/main.go index 3f52026..452eac8 100644 --- a/examples/basic/main.go +++ b/examples/basic/main.go @@ -45,5 +45,8 @@ func main() { }) }) - router.Run(":8080") + err = router.Run(":8080") + if err != nil { + log.Fatal("Could not start server") + } } diff --git a/examples/m2m/go.mod b/examples/m2m/go.mod new file mode 100644 index 0000000..faf36da --- /dev/null +++ b/examples/m2m/go.mod @@ -0,0 +1,3 @@ +module example + +go 1.23.4 diff --git a/examples/m2m/main.go b/examples/m2m/main.go new file mode 100644 index 0000000..d19e57a --- /dev/null +++ b/examples/m2m/main.go @@ -0,0 +1,125 @@ +package main + +import ( + "crypto/tls" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "strconv" + "strings" +) + +// TokenConfig represents the configuration for token retrieval +type TokenConfig struct { + BaseURL string + ClientID string + ServiceAccount string + ServiceToken string + SkipTLSVerify bool +} + +// TokenResponse represents the structure of the token response +type TokenResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` +} + +// NewTokenConfig creates a configuration from environment variables +func NewTokenConfig() *TokenConfig { + skipTlsVerify, _ := strconv.ParseBool(getEnv("SKIP_TLS_VERIFY", "false")) + return &TokenConfig{ + BaseURL: getEnv("BASE_URL", "https://localhost:9443"), + ClientID: getEnv("CLIENT_ID", ""), + ServiceAccount: getEnv("SERVICE_ACCOUNT", ""), + ServiceToken: getEnv("SERVICE_TOKEN", ""), + SkipTLSVerify: skipTlsVerify, + } +} + +// getEnv retrieves an environment variable with a default value +func getEnv(key, defaultValue string) string { + value := os.Getenv(key) + if value == "" { + return defaultValue + } + return value +} + +// getAccessToken retrieves an access token from the specified endpoint +func getAccessToken(config *TokenConfig) (*TokenResponse, error) { + // Validate required configuration + if config.ClientID == "" || config.ServiceAccount == "" || config.ServiceToken == "" { + return nil, fmt.Errorf("missing required configuration") + } + + // Create a custom HTTP client with optional TLS verification + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: config.SkipTLSVerify}, + } + client := &http.Client{Transport: tr} + + // Prepare the request body + data := url.Values{ + "grant_type": {"client_credentials"}, + "client_id": {config.ClientID}, + "username": {config.ServiceAccount}, + "password": {config.ServiceToken}, + "scope": {"profile"}, + } + + // Create the HTTP request + fullURL := strings.TrimRight(config.BaseURL, "/") + "/application/o/token/" + req, err := http.NewRequest("POST", fullURL, strings.NewReader(data.Encode())) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + // Send the request + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("error sending request: %w", err) + } + defer resp.Body.Close() + + // Read the response body + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading response: %w", err) + } + + // Check for successful response + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("token retrieval failed (status %d): %s", + resp.StatusCode, string(body)) + } + + // Parse the JSON response + var tokenResp TokenResponse + if err := json.Unmarshal(body, &tokenResp); err != nil { + return nil, fmt.Errorf("error parsing token response: %w\nRaw response: %s", err, string(body)) + } + + return &tokenResp, nil +} + +func main() { + // Load configuration from environment + config := NewTokenConfig() + + // Retrieve access token + tokenResp, err := getAccessToken(config) + if err != nil { + log.Fatalf("Failed to obtain access token: %v", err) + } + + // Print token details + fmt.Println("Access Token:", tokenResp.AccessToken) + fmt.Println("Token Type:", tokenResp.TokenType) + fmt.Println("Expires In:", tokenResp.ExpiresIn, "seconds") +} diff --git a/examples/m2m/readme.md b/examples/m2m/readme.md new file mode 100644 index 0000000..3c7fdc0 --- /dev/null +++ b/examples/m2m/readme.md @@ -0,0 +1,68 @@ + +# Authentik OAuth Token Retrieval Client (M2M) + +This repository provides an example implementation of a Machine-to-Machine (M2M) application using the [Authentik OAuth2 Client Credentials](https://docs.goauthentik.io/docs/add-secure-apps/providers/oauth2/client_credentials) provider. + +## Configuration + +### Environment Variables + +The following environment variables must be set to configure the application: + +- **`BASE_URL`**: + Base URL of the authentication server. + *Default*: `https://localhost:9443` + *Example*: `https://your-auth-server.com` + +- **`CLIENT_ID`**: + OAuth client identifier. + *Required* (no default value). + *Example*: `WxUcBMGZdI7c0e5oYp6mYdEd64acpXSuWKh8zBH5` + +- **`SERVICE_ACCOUNT`**: + Service account username. + *Required* (no default value). + *Example*: `test2test` + +- **`SERVICE_TOKEN`**: + Service account password/token. + *Required* (no default value). + *Example*: `2Dzvbs5O7wjfaUj1k1YSqctRgVA5hDtsi18xIrmKeIn1pV0rn4G5nuuFQUwH` + +- **`SKIP_TLS_VERIFY`**: + Disable TLS certificate verification. + *Default*: `false` + +## Usage + +### Setting Environment Variables + +Set the required environment variables before running the application. For example: + +```bash +export BASE_URL=https://localhost:9443 +export CLIENT_ID=WxUcBMGZdI7c0e5oYp6mYdEd64acpXSuWKh8zBH5 +export SERVICE_ACCOUNT=test2test +export SERVICE_TOKEN=2Dzvbs5O7wjfaUj1k1YSqctRgVA5hDtsi18xIrmKeIn1pV0rn4G5nuuFQUwH +export SKIP_TLS_VERIFY=true +``` + +## Running the script + +Run the script using the following command: + +`go run main.go` + +Example Output + +When the application is successfully run, you will see output similar to the following: + +``` +Access Token: +Token Type: Bearer +Expires In: 3600 seconds +``` + +The retrieved access token can then be used for authorized access to other resources. + +For more details, refer to the [Authentik](https://goauthentik.io/) Documentation. diff --git a/examples/validation/main.go b/examples/validation/main.go index 09417f7..8f28d82 100644 --- a/examples/validation/main.go +++ b/examples/validation/main.go @@ -47,5 +47,8 @@ func main() { }) }) - router.Run(":8080") + err = router.Run(":8080") + if err != nil { + log.Fatal("Could not start server") + } }