181 lines
5.8 KiB
Go
181 lines
5.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"github.com/zitadel/logging"
|
|
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
|
httphelper "github.com/zitadel/oidc/v3/pkg/http"
|
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
|
)
|
|
|
|
var (
|
|
callbackPath = "/auth/callback"
|
|
key = []byte("test1234test1234")
|
|
)
|
|
|
|
func main() {
|
|
clientID := os.Getenv("CLIENT_ID")
|
|
clientSecret := os.Getenv("CLIENT_SECRET")
|
|
keyPath := os.Getenv("KEY_PATH")
|
|
issuer := os.Getenv("ISSUER")
|
|
port := os.Getenv("PORT")
|
|
scopes := strings.Split(os.Getenv("SCOPES"), " ")
|
|
responseMode := os.Getenv("RESPONSE_MODE")
|
|
|
|
redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath)
|
|
cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure())
|
|
|
|
logger := slog.New(
|
|
slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
|
AddSource: true,
|
|
Level: slog.LevelDebug,
|
|
}),
|
|
)
|
|
client := &http.Client{
|
|
Timeout: time.Minute,
|
|
}
|
|
// enable outgoing request logging
|
|
logging.EnableHTTPClient(client,
|
|
logging.WithClientGroup("client"),
|
|
)
|
|
|
|
options := []rp.Option{
|
|
rp.WithCookieHandler(cookieHandler),
|
|
rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)),
|
|
rp.WithHTTPClient(client),
|
|
rp.WithLogger(logger),
|
|
rp.WithSigningAlgsFromDiscovery(),
|
|
}
|
|
if clientSecret == "" {
|
|
options = append(options, rp.WithPKCE(cookieHandler))
|
|
}
|
|
if keyPath != "" {
|
|
options = append(options, rp.WithJWTProfile(rp.SignerFromKeyPath(keyPath)))
|
|
}
|
|
|
|
// One can add a logger to the context,
|
|
// pre-defining log attributes as required.
|
|
ctx := logging.ToContext(context.TODO(), logger)
|
|
provider, err := rp.NewRelyingPartyOIDC(ctx, issuer, clientID, clientSecret, redirectURI, scopes, options...)
|
|
if err != nil {
|
|
logrus.Fatalf("error creating provider %s", err.Error())
|
|
}
|
|
|
|
// generate some state (representing the state of the user in your application,
|
|
// e.g. the page where he was before sending him to login
|
|
state := func() string {
|
|
return uuid.New().String()
|
|
}
|
|
|
|
urlOptions := []rp.URLParamOpt{
|
|
rp.WithPromptURLParam("Welcome back!"),
|
|
}
|
|
|
|
if responseMode != "" {
|
|
urlOptions = append(urlOptions, rp.WithResponseModeURLParam(oidc.ResponseMode(responseMode)))
|
|
}
|
|
|
|
// register the AuthURLHandler at your preferred path.
|
|
// the AuthURLHandler creates the auth request and redirects the user to the auth server.
|
|
// including state handling with secure cookie and the possibility to use PKCE.
|
|
// Prompts can optionally be set to inform the server of
|
|
// any messages that need to be prompted back to the user.
|
|
http.Handle("/login", rp.AuthURLHandler(
|
|
state,
|
|
provider,
|
|
urlOptions...,
|
|
))
|
|
|
|
// for demonstration purposes the returned userinfo response is written as JSON object onto response
|
|
marshalUserinfo := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) {
|
|
fmt.Println("access token", tokens.AccessToken)
|
|
fmt.Println("refresh token", tokens.RefreshToken)
|
|
fmt.Println("id token", tokens.IDToken)
|
|
|
|
data, err := json.Marshal(info)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("content-type", "application/json")
|
|
w.Write(data)
|
|
}
|
|
|
|
// you could also just take the access_token and id_token without calling the userinfo endpoint:
|
|
//
|
|
// marshalToken := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty) {
|
|
// data, err := json.Marshal(tokens)
|
|
// if err != nil {
|
|
// http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
// return
|
|
// }
|
|
// w.Write(data)
|
|
//}
|
|
|
|
// you can also try token exchange flow
|
|
//
|
|
// requestTokenExchange := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty, info oidc.UserInfo) {
|
|
// data := make(url.Values)
|
|
// data.Set("grant_type", string(oidc.GrantTypeTokenExchange))
|
|
// data.Set("requested_token_type", string(oidc.IDTokenType))
|
|
// data.Set("subject_token", tokens.RefreshToken)
|
|
// data.Set("subject_token_type", string(oidc.RefreshTokenType))
|
|
// data.Add("scope", "profile custom_scope:impersonate:id2")
|
|
|
|
// client := &http.Client{}
|
|
// r2, _ := http.NewRequest(http.MethodPost, issuer+"/oauth/token", strings.NewReader(data.Encode()))
|
|
// // r2.Header.Add("Authorization", "Basic "+"d2ViOnNlY3JldA==")
|
|
// r2.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
// r2.SetBasicAuth("web", "secret")
|
|
|
|
// resp, _ := client.Do(r2)
|
|
// fmt.Println(resp.Status)
|
|
|
|
// b, _ := io.ReadAll(resp.Body)
|
|
// resp.Body.Close()
|
|
|
|
// w.Write(b)
|
|
// }
|
|
|
|
// register the CodeExchangeHandler at the callbackPath
|
|
// the CodeExchangeHandler handles the auth response, creates the token request and calls the callback function
|
|
// with the returned tokens from the token endpoint
|
|
// in this example the callback function itself is wrapped by the UserinfoCallback which
|
|
// will call the Userinfo endpoint, check the sub and pass the info into the callback function
|
|
http.Handle(callbackPath, rp.CodeExchangeHandler(rp.UserinfoCallback(marshalUserinfo), provider))
|
|
|
|
// if you would use the callback without calling the userinfo endpoint, simply switch the callback handler for:
|
|
//
|
|
// http.Handle(callbackPath, rp.CodeExchangeHandler(marshalToken, provider))
|
|
|
|
// simple counter for request IDs
|
|
var counter atomic.Int64
|
|
// enable incomming request logging
|
|
mw := logging.Middleware(
|
|
logging.WithLogger(logger),
|
|
logging.WithGroup("server"),
|
|
logging.WithIDFunc(func() slog.Attr {
|
|
return slog.Int64("id", counter.Add(1))
|
|
}),
|
|
)
|
|
|
|
lis := fmt.Sprintf("127.0.0.1:%s", port)
|
|
logger.Info("server listening, press ctrl+c to stop", "addr", lis)
|
|
err = http.ListenAndServe(lis, mw(http.DefaultServeMux))
|
|
if err != http.ErrServerClosed {
|
|
logger.Error("server terminated", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|