From 2964391f0ac99e1c2585bf9be6a7083bc274185f Mon Sep 17 00:00:00 2001 From: Mike Conrad Date: Mon, 14 Apr 2025 14:51:42 -0400 Subject: [PATCH] Initial example authentication to Azure ad --- .gitignore | 2 + go.mod | 14 ++++++ go.sum | 22 ++++++++++ main.go | 126 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 164 insertions(+) create mode 100644 .gitignore create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..caea723 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +setup.sh +.env diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fc7dd99 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module example/hello + +go 1.24.2 + +require ( + github.com/coreos/go-oidc v2.3.0+incompatible + golang.org/x/oauth2 v0.29.0 +) + +require ( + github.com/pquerna/cachecontrol v0.2.0 // indirect + golang.org/x/crypto v0.37.0 // indirect + gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..668ca51 --- /dev/null +++ b/go.sum @@ -0,0 +1,22 @@ +github.com/coreos/go-oidc v2.3.0+incompatible h1:+5vEsrgprdLjjQ9FzIKAzQz1wwPD+83hQRfUIPh7rO0= +github.com/coreos/go-oidc v2.3.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/cachecontrol v0.2.0 h1:vBXSNuE5MYP9IJ5kjsdo8uq+w41jSPgvba2DEnkRx9k= +github.com/pquerna/cachecontrol v0.2.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= +golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs= +gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..6396ddb --- /dev/null +++ b/main.go @@ -0,0 +1,126 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "strings" + "os" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/microsoft" + "github.com/coreos/go-oidc" +) + +var ( + clientID = os.Getenv("AZURE_CLIENT_ID") + clientSecret = os.Getenv("AZURE_CLIENT_SECRET") + redirectURL = os.Getenv("AZURE_REDIRECT_URL") // e.g., http://localhost:5000/callback + tenantID = os.Getenv("AZURE_TENANT_ID") + + provider *oidc.Provider + verifier *oidc.IDTokenVerifier + oauth2Config *oauth2.Config +) + +func main() { + + ctx := context.Background() + issuer := fmt.Sprintf("https://login.microsoftonline.com/%s/v2.0", tenantID) + + var err error + provider, err = oidc.NewProvider(ctx, issuer) + if err != nil { + log.Fatalf("failed to get provider: %v", err) + } + + verifier = provider.Verifier(&oidc.Config{ClientID: clientID}) + + oauth2Config = &oauth2.Config{ + ClientID: clientID, + ClientSecret: clientSecret, + Endpoint: microsoft.AzureADEndpoint(tenantID), + RedirectURL: redirectURL, + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + } + + http.HandleFunc("/", handleIndex) + http.HandleFunc("/callback", handleCallback) + + log.Println("Server started at http://localhost:5000") + log.Fatal(http.ListenAndServe(":5000", nil)) +} + +func handleIndex(w http.ResponseWriter, r *http.Request) { + idTokenCookie, err := r.Cookie("id_token") + if err != nil { + log.Println("No id_token cookie found:", err) + http.Redirect(w, r, oauth2Config.AuthCodeURL("state", oauth2.AccessTypeOffline), http.StatusFound) + return + } + + token, err := verifier.Verify(r.Context(), idTokenCookie.Value) + if err != nil { + log.Println("Token verification failed:", err) + // Clear the invalid cookie to avoid another loop + http.SetCookie(w, &http.Cookie{ + Name: "id_token", + Value: "", + Path: "/", + MaxAge: -1, + HttpOnly: true, + }) + fmt.Printf("Tokent: %s", token) + http.Redirect(w, r, oauth2Config.AuthCodeURL("state", oauth2.AccessTypeOffline), http.StatusFound) + return + } + + var claims struct { + Email string `json:"email"` + PreferredUsername string `json:"preferred_username"` + Name string `json:"name"` + } + if err := token.Claims(&claims); err != nil { + http.Error(w, "Failed to parse claims", http.StatusInternalServerError) + return + } + email := claims.Email + if email == "" { + email = strings.Split(claims.PreferredUsername, "@")[0] + } + fmt.Fprintf(w, "Welcome! Logged in as: %s", email) +} + +func handleCallback(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + code := r.URL.Query().Get("code") + + token, err := oauth2Config.Exchange(ctx, code) + if err != nil { + http.Error(w, "Token exchange failed", http.StatusInternalServerError) + return + } + + rawIDToken, ok := token.Extra("id_token").(string) + if !ok { + http.Error(w, "No id_token found", http.StatusInternalServerError) + return + } + + // Verify and set as cookie + _, err = verifier.Verify(ctx, rawIDToken) + if err != nil { + http.Error(w, "Failed to verify ID token", http.StatusUnauthorized) + return + } + + // Set token as cookie + http.SetCookie(w, &http.Cookie{ + Name: "id_token", + Value: rawIDToken, + Path: "/", + HttpOnly: true, + }) + http.Redirect(w, r, "/", http.StatusFound) +}