Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OAuth 2.0 support into the example app #569

Merged
merged 1 commit into from
Sep 20, 2023
Merged
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
Add OAuth 2.0 support into the example app
TheSpiritXIII committed Sep 19, 2023

Verified

This commit was signed with the committer’s verified signature.
TheSpiritXIII Daniel Hrabovcak
commit 6a590616356a28867c4f91ad85d1d8c985b290db
9 changes: 9 additions & 0 deletions examples/instrumentation/go-synthetic/README.md
Original file line number Diff line number Diff line change
@@ -41,6 +41,15 @@ go run ./examples/instrumentation/go-synthetic/ --auth-scheme=Bearer --auth-para
curl -H "Authorization: Bearer xyz" localhost:8080/metrics
```

#### OAuth 2.0

```bash
go run ./examples/instrumentation/go-synthetic/ --oauth2-client-id=abc --oauth2-client-secret=xyz
curl "localhost:8080/token?grant_type=client_credentials&client_id=abc&client_secret=xyz"
# Fetch access token from above and use as bearer token example below:
curl -H "Authorization: Bearer DZ~9UYwD" localhost:8080/metrics
```

## Running on Kubernetes

If running managed-collection on a Kubernetes cluster, the `go-synthetic` can be
138 changes: 130 additions & 8 deletions examples/instrumentation/go-synthetic/auth.go
Original file line number Diff line number Diff line change
@@ -3,9 +3,25 @@ package main
import (
"errors"
"flag"
"fmt"
"math/rand"
"net/http"
"sort"
"strings"

"github.com/google/go-cmp/cmp"
)

func isFlagSet(name string) bool {
found := false
flag.Visit(func(f *flag.Flag) {
if f.Name == name {
found = true
}
})
return found
}

type basicAuthConfig struct {
username string
password string
@@ -35,6 +51,20 @@ func (c *basicAuthConfig) handle(handler http.Handler) http.Handler {
})
}

func authorizationHandler(handler http.Handler, scheme, parameters string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
expected := scheme + " " + parameters
if auth == expected {
handler.ServeHTTP(w, r)
return
}

w.Header().Set("WWW-Authenticate", scheme+` realm="restricted", charset="UTF-8"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
})
}

type authorizationConfig struct {
scheme string
parameters string
@@ -63,43 +93,135 @@ func (c *authorizationConfig) validate() error {
}

func (c *authorizationConfig) handle(handler http.Handler) http.Handler {
return authorizationHandler(handler, c.scheme, c.parameters)
}

type oauth2Config struct {
clientID string
clientSecret string
scopes string
accessToken string
}

func newOAuth2ConfigFromFlags() *oauth2Config {
c := &oauth2Config{}
flag.StringVar(&c.clientID, "oauth2-client-id", "", "OAuth2 client ID")
flag.StringVar(&c.clientSecret, "oauth2-client-secret", "", "OAuth2 client secret")
flag.StringVar(&c.scopes, "oauth2-scopes", "", "Required OAuth2 comma-separated scopes")
flag.StringVar(&c.accessToken, "oauth2-access-token", "", "OAuth2 access token or empty to generate one. /token will provision this token")
return c
}

func (c *oauth2Config) isEnabled() bool {
return c.clientID != "" || c.clientSecret != "" || c.scopes != "" || isFlagSet("oauth2-access-token")
}

const oauth2TokenCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~+/"
const defaultOAuth2TokenLength = 8

func (c *oauth2Config) validate() error {
if c.accessToken == "" {
builder := strings.Builder{}
builder.Grow(defaultOAuth2TokenLength)
for i := 0; i < builder.Cap(); i++ {
builder.WriteByte(oauth2TokenCharset[rand.Intn(len(oauth2TokenCharset))])
}
c.accessToken = builder.String()
}
return nil
}

func oauthTokenErrorResponse(errorCode string) []byte {
return []byte(fmt.Sprintf("{\n\terror: %q,\n}\n", errorCode))
}

func (c *oauth2Config) tokenHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
expected := c.scheme + " " + c.parameters
if auth == expected {
handler.ServeHTTP(w, r)
w.Header().Set("Content-Type", "application/json")

grantType := r.URL.Query().Get("grant_type")
clientID := r.URL.Query().Get("client_id")
clientSecret := r.URL.Query().Get("client_secret")
scopes := r.URL.Query().Get("scope")
if grantType != "client_credentials" {
w.WriteHeader(http.StatusBadRequest)
w.Write(oauthTokenErrorResponse("unsupported_grant_type"))
return
}

w.Header().Set("WWW-Authenticate", c.scheme+` realm="restricted", charset="UTF-8"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
if clientID != c.clientID || clientSecret != c.clientSecret {
w.WriteHeader(http.StatusUnauthorized)
w.Write(oauthTokenErrorResponse("invalid_client"))
return
}

if len(c.scopes) > 0 {
requiredScopes := strings.Split(c.scopes, ",")
sort.Strings(requiredScopes)
requestedScopes := strings.Split(scopes, " ")
sort.Strings(requestedScopes)
if !cmp.Equal(requestedScopes, requiredScopes) {
w.WriteHeader(http.StatusUnauthorized)
w.Write(oauthTokenErrorResponse("invalid_scope"))
return
}
}

response := fmt.Sprintf("{\n\taccess_token: %q,\n\ttoken_type: %q\n}\n", c.accessToken, "bearer")
w.Write([]byte(response))
})
}

func (c *oauth2Config) handle(handler http.Handler) http.Handler {
return authorizationHandler(handler, "Bearer", c.accessToken)
}

type httpClientConfig struct {
basicAuth *basicAuthConfig
auth *authorizationConfig
oauth2 *oauth2Config
}

func newHttpClientConfigFromFlags() *httpClientConfig {
return &httpClientConfig{
basicAuth: newBasicAuthConfigFromFlags(),
auth: newAuthorizationConfigFromFlags(),
oauth2: newOAuth2ConfigFromFlags(),
}
}

func (c *httpClientConfig) validate() error {
var errs []error
if c.basicAuth.isEnabled() && c.auth.isEnabled() {
errs = append(errs, errors.New("cannot specify both --basic-auth and --auth flags"))
if c.basicAuth.isEnabled() {
if c.auth.isEnabled() {
errs = append(errs, errors.New("cannot specify both --basic-auth and --auth flags"))
}
if c.oauth2.isEnabled() {
errs = append(errs, errors.New("cannot specify both --basic-auth and --oauth2 flags"))
}
}
if c.auth.isEnabled() && c.oauth2.isEnabled() {
errs = append(errs, errors.New("cannot specify both --auth and --oa2uth flags"))
}
if err := c.auth.validate(); err != nil {
errs = append(errs, err)
}
if err := c.oauth2.validate(); err != nil {
errs = append(errs, err)
}
return errors.Join(errs...)
}

func (c *httpClientConfig) register(mux *http.ServeMux) {
if c.oauth2.isEnabled() {
mux.Handle("/token", c.oauth2.tokenHandler())
}
}

func (c *httpClientConfig) handle(handler http.Handler) http.Handler {
if c.oauth2.isEnabled() {
return c.oauth2.handle(handler)
}
if c.auth.isEnabled() {
return c.auth.handle(handler)
}
1 change: 1 addition & 0 deletions examples/instrumentation/go-synthetic/main.go
Original file line number Diff line number Diff line change
@@ -228,6 +228,7 @@ func main() {
Registry: metrics,
EnableOpenMetrics: true,
})))
httpClientConfig.register(mux)

server := &http.Server{
Addr: *addr,