From edeefd659810738f3589e74937133b93820ae307 Mon Sep 17 00:00:00 2001 From: Enrico Regge Date: Thu, 27 Mar 2025 16:27:54 +0100 Subject: [PATCH 1/9] first draft of OIDC samples --- oidc/.gitignore | 1 + oidc/README.md | 49 ++++ oidc/go/.ceignore | 1 + oidc/go/Dockerfile | 11 + oidc/go/app.go | 388 ++++++++++++++++++++++++++++++ oidc/go/build | 19 ++ oidc/go/go.mod | 8 + oidc/go/go.sum | 6 + oidc/go/static/auth-failed.html | 29 +++ oidc/go/static/auth-logout.html | 16 ++ oidc/go/static/css/style.css | 13 + oidc/go/static/home.html | 39 +++ oidc/go/static/images/favicon.ico | Bin 0 -> 2247 bytes 13 files changed, 580 insertions(+) create mode 100644 oidc/.gitignore create mode 100644 oidc/README.md create mode 100644 oidc/go/.ceignore create mode 100644 oidc/go/Dockerfile create mode 100644 oidc/go/app.go create mode 100755 oidc/go/build create mode 100644 oidc/go/go.mod create mode 100644 oidc/go/go.sum create mode 100644 oidc/go/static/auth-failed.html create mode 100644 oidc/go/static/auth-logout.html create mode 100644 oidc/go/static/css/style.css create mode 100644 oidc/go/static/home.html create mode 100644 oidc/go/static/images/favicon.ico diff --git a/oidc/.gitignore b/oidc/.gitignore new file mode 100644 index 000000000..2eea525d8 --- /dev/null +++ b/oidc/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/oidc/README.md b/oidc/README.md new file mode 100644 index 000000000..fcb092534 --- /dev/null +++ b/oidc/README.md @@ -0,0 +1,49 @@ +# OAuth2 / OIDC + +A pretty simple golang application that, in its most basic form, will +return "Hello World" back to the caller. + +Check the source code for all of the things you can make it do either via +environment variables or query parameters. This is good for testing the +system to see how it reacts - for example, when the app crashes. + +Note: we added some extra logic to this so I can also be used as a batch job +but you can ignore that if all you care about is the App side of things. + + +``` +OIDC_CLIENT_ID= > .env +OIDC_CLIENT_SECRET= >> .env +OIDC_PROVIDER_AUTHORIZATION_ENDPOINT= >> .env +OIDC_PROVIDER_TOKEN_ENDPOINT= >> .env +OIDC_PROVIDER_USERINFO_ENDPOINT= >> .env +``` + +* Create the secret +``` +ibmcloud ce secret create --name oidc-credentials --from-env-file .env +``` + +* Create the application +``` +LANGUAGE=go +cd $LANGUAGE +ibmcloud ce app create --name oidc-sample-$LANGUAGE --src . \ + --cpu 0.125 \ + --memory 0.25G \ + --env-from-secret oidc-credentials + +OIDC_REDIRECT_URL=$(ibmcloud ce app get -n oidc-sample-$LANGUAGE --output url) +ibmcloud ce app update --name oidc-sample-$LANGUAGE --env OIDC_REDIRECT_URL=$OIDC_REDIRECT_URL/auth/callback +cd .. +``` + +- - - + +As noted in [the main README](../README.md), this sample has two pieces: + +- a `build` script which will build the container image(s) used +- a `run` script which deploys resources that use those images + +The main purpose of this example is the `run` script, but the `build` +script is included for complete educational (and reuse) purposes. diff --git a/oidc/go/.ceignore b/oidc/go/.ceignore new file mode 100644 index 000000000..2eea525d8 --- /dev/null +++ b/oidc/go/.ceignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/oidc/go/Dockerfile b/oidc/go/Dockerfile new file mode 100644 index 000000000..71a9de7eb --- /dev/null +++ b/oidc/go/Dockerfile @@ -0,0 +1,11 @@ +FROM quay.io/projectquay/golang:1.22 AS build-env +WORKDIR /go/src/app +COPY . . + +RUN CGO_ENABLED=0 GOOS=linux go build -o /go/bin/app app.go + +# Copy the exe into a smaller base image +FROM gcr.io/distroless/static-debian12 +COPY --from=build-env /go/bin/app / +COPY static /static +ENTRYPOINT ["/app"] diff --git a/oidc/go/app.go b/oidc/go/app.go new file mode 100644 index 000000000..e13498c1e --- /dev/null +++ b/oidc/go/app.go @@ -0,0 +1,388 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "html/template" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "golang.org/x/net/context" + "golang.org/x/oauth2" +) + +var conf oauth2.Config +var oidcConfig OIDCConfig + +const STATE = "state" +const SESSION_TOKEN = "session_token" + +// Home struct, used for home.html template +type Home struct { + Title string + User User + ClientId string + ProviderAuthorizationEndpoint string + ProviderTokenEndpoint string + ProviderUserInfoEndpoint string +} + +// User struct, holds all the user info shown in home.html +type User struct { + Token string + Profile string +} + +// OIDC configuration struct +type OIDCConfig struct { + ClientId string + ClientSecret string + ProviderAuthorizationEndpoint string + ProviderTokenEndpoint string + ProviderUserInfoEndpoint string + RedirectUrl string +} + +// Loads the OIDC configuration from environment variables +func loadOIDCConfig() (oidcConfiguration OIDCConfig, err error) { + oidcConfiguration.ClientId = os.Getenv("OIDC_CLIENT_ID") + if oidcConfiguration.ClientId == "" { + err = fmt.Errorf("missing %s environment variable", "OIDC_CLIENT_ID") + return + } + + oidcConfiguration.ClientSecret = os.Getenv("OIDC_CLIENT_SECRET") + if oidcConfiguration.ClientSecret == "" { + err = fmt.Errorf("missing %s environment variable", "OIDC_CLIENT_SECRET") + return + } + oidcConfiguration.ProviderAuthorizationEndpoint = os.Getenv("OIDC_PROVIDER_AUTHORIZATION_ENDPOINT") + if oidcConfiguration.ProviderAuthorizationEndpoint == "" { + err = fmt.Errorf("missing %s environment variable", "OIDC_PROVIDER_AUTHORIZATION_ENDPOINT") + return + } + oidcConfiguration.ProviderTokenEndpoint = os.Getenv("OIDC_PROVIDER_TOKEN_ENDPOINT") + if oidcConfiguration.ProviderTokenEndpoint == "" { + err = fmt.Errorf("missing %s environment variable", "OIDC_PROVIDER_TOKEN_ENDPOINT") + return + } + oidcConfiguration.ProviderUserInfoEndpoint = os.Getenv("OIDC_PROVIDER_USERINFO_ENDPOINT") + if oidcConfiguration.ProviderUserInfoEndpoint == "" { + err = fmt.Errorf("missing %s environment variable", "OIDC_PROVIDER_USERINFO_ENDPOINT") + return + } + oidcConfiguration.RedirectUrl = os.Getenv("OIDC_REDIRECT_URL") + if oidcConfiguration.RedirectUrl == "" { + err = fmt.Errorf("missing %s environment variable", "OIDC_REDIRECT_URL") + return + } + return oidcConfiguration, nil +} + +// Requests an OAuthToken using a "code" type +func GetOauthToken(r *http.Request) (*oauth2.Token, error) { + + log.Println("Getting auth token.") + + ctx := context.Background() + + if ctx == nil { + return nil, errors.New("could not get context") + } + + if r.URL.Query().Get(STATE) != STATE { + return nil, errors.New("state value did not match") + } + + // Exchange code for OAuth token + oauth2Token, oauth2TokenError := conf.Exchange(ctx, r.URL.Query().Get("code")) + if oauth2TokenError != nil { + return nil, errors.New("Failed to exchange token:" + oauth2TokenError.Error()) + } + + return oauth2Token, nil +} + +// Requests a user profile, using a bearer token +func GetUserProfile(r *http.Request, token oauth2.Token) (interface{}, error) { + + log.Println("Getting user profile ...") + + ctx := context.Background() + + if ctx == nil { + return nil, errors.New("could not get context") + } + + // Getting now the userInfo + client := conf.Client(ctx, &token) + + // Get request using /userinfo url + userinfoResponse, userinfoError := client.Get(oidcConfig.ProviderUserInfoEndpoint) + if userinfoError != nil { + return nil, errors.New("Failed to obtain userinfo:" + userinfoError.Error()) + } + + defer userinfoResponse.Body.Close() + + log.Println("Getting user profile: " + userinfoResponse.Status) + + if userinfoResponse.StatusCode != http.StatusOK { + return nil, errors.New("HTTP status is not 200. Was " + userinfoResponse.Status + "; response: " + toJSONString(userinfoResponse.Body)) + } + + // Decoding profile info and putting it in a map, to make it more readable + var profile map[string]interface{} + if userinfoError = json.NewDecoder(userinfoResponse.Body).Decode(&profile); userinfoError != nil { + return nil, userinfoError + } + + return profile, nil + +} + +// Home handler for /home +func home(w http.ResponseWriter, r *http.Request) { + + log.Printf("Executing /home for '%s'", r.RequestURI) + + // Parssing home.html template + tmpl, _ := template.ParseFiles("./static/home.html") + data := &Home{} + + // Adding title to page + data.Title = "OIDC sample - IBM Cloud Code Engine" + + // Getting cookie named SESSION_TOKEN + cookie, err := r.Cookie(SESSION_TOKEN) + + if err != nil { + + // If no cookie found, that's ok, that means no user is logged in + log.Println("No session cookie found:" + err.Error()) + + // Redirecting to /, in order to show the logged in user values + http.Redirect(w, r, "/auth/login", http.StatusSeeOther) + } else { + + log.Println("Session cookie found.") + + // A cookie was found, this means a user is logged in + // Let's get the auth token value + + authToken := oauth2.Token{ + AccessToken: cookie.Value, + } + + // Getting the user profile for the given auth token + profile, profileError := GetUserProfile(r, authToken) + + if profileError != nil { + log.Print("Error getting profile. Error: " + profileError.Error()) + + // Redirecting to /auth/failed, in order to avoid an endless redirect loop + http.Redirect(w, r, "/auth/failed", http.StatusSeeOther) + return + } + + // Exposing OIDC configuration values + data.ClientId = oidcConfig.ClientId + data.ProviderAuthorizationEndpoint = oidcConfig.ProviderAuthorizationEndpoint + data.ProviderTokenEndpoint = oidcConfig.ProviderTokenEndpoint + data.ProviderUserInfoEndpoint = oidcConfig.ProviderUserInfoEndpoint + + // Setting values in page template, this is what we are going to show for the logged in user + data.User.Token = fmt.Sprintln(authToken.AccessToken) + data.User.Profile = fmt.Sprintln(profile) + + log.Println("User already logged in:" + fmt.Sprintln(profile)) + + } + + tmpl.ExecuteTemplate(w, "home", data) + +} + +// Home handler for /auth/failed +func authFailed(w http.ResponseWriter, r *http.Request) { + + log.Println("Executing /auth/failed") + + // Parssing auth-failed.html template + tmpl, _ := template.ParseFiles("./static/auth-failed.html") + data := &Home{} + + // Adding title to page + data.Title = "Authentication Failed" + + // Exposing OIDC configuration values + data.ClientId = oidcConfig.ClientId + data.ProviderAuthorizationEndpoint = oidcConfig.ProviderAuthorizationEndpoint + data.ProviderTokenEndpoint = oidcConfig.ProviderTokenEndpoint + data.ProviderUserInfoEndpoint = oidcConfig.ProviderUserInfoEndpoint + + w.WriteHeader(http.StatusUnauthorized) + tmpl.ExecuteTemplate(w, "authFailed", data) +} + +// Login handler for /auth/login +func authLogin(w http.ResponseWriter, r *http.Request) { + + log.Println("Executing /auth/login") + + // Code request to Auth URL + http.Redirect(w, r, conf.AuthCodeURL(STATE), http.StatusFound) +} + +// Handler for /auth/callback +func authCallback(w http.ResponseWriter, r *http.Request) { + + log.Println("Executing /auth/callback") + + // Getting auth token from request + authToken, error := GetOauthToken(r) + + if error != nil { + + log.Println("Error getting auth token. Error: " + error.Error()) + + // Redirecting to /auth/failed, in order to avoid an endless redirect loop + http.Redirect(w, r, "/auth/failed", http.StatusSeeOther) + return + } + + log.Println("Setting session cookie.") + + // Setting cookie with the value of this auth token + + http.SetCookie(w, &http.Cookie{ + Name: "session_token", + Value: authToken.AccessToken, + Path: "/", + Expires: time.Now().Add(1000 * time.Second), + HttpOnly: true, + Secure: true, + }) + + // Redirecting to /, in order to show the logged in user values + http.Redirect(w, r, "/", http.StatusSeeOther) +} + +// Logout handler for /auth/logout +func authLogout(w http.ResponseWriter, r *http.Request) { + + log.Println("Executing /auth/logout") + + // Parssing auth-none.html template + tmpl, _ := template.ParseFiles("./static/auth-logout.html") + data := &Home{} + + // Adding title to page + data.Title = "Logged out" + + // Getting session cookie + cookie, err := r.Cookie(SESSION_TOKEN) + + if err != nil { + + log.Println("No session cookie found:" + err.Error()) + + } else { + + log.Println("Session cookie found, invalidating it.") + + // If cookie was found, let's invalidate it + cookie.Value = "" + cookie.Expires = time.Unix(0, 0) + cookie.MaxAge = -1 + cookie.HttpOnly = true + } + + // Setting the invalidated cookie + http.SetCookie(w, cookie) + w.WriteHeader(http.StatusFound) + tmpl.ExecuteTemplate(w, "authLogout", data) +} + +func main() { + ctx := context.Background() + signals := make(chan os.Signal, 1) + signal.Notify(signals, os.Interrupt, syscall.SIGTERM) + log.Println("Starting app ...") + + // Load OIDC relevant config parameters + var err error + oidcConfig, err = loadOIDCConfig() + if err != nil { + log.Println("Aborting! Could not load OIDC config. Error: " + err.Error()) + os.Exit(1) + + } + + log.Println("Redirect URL: '" + oidcConfig.RedirectUrl + "'") + + // Building global conf object, using OAuth2/OIDC configuration + conf = oauth2.Config{ + ClientID: oidcConfig.ClientId, + ClientSecret: oidcConfig.ClientSecret, + RedirectURL: oidcConfig.RedirectUrl, + Scopes: []string{"openid", "profile"}, + Endpoint: oauth2.Endpoint{ + AuthURL: oidcConfig.ProviderAuthorizationEndpoint, + TokenURL: oidcConfig.ProviderTokenEndpoint, + }, + } + + // Serving static files + fs := http.FileServer(http.Dir("static")) + + // Creating handlers: /static /home /login /auth/callback /logout + http.Handle("/static/", http.StripPrefix("/static/", fs)) + http.HandleFunc("/auth/login", authLogin) + http.HandleFunc("/auth/callback", authCallback) + http.HandleFunc("/auth/logout", authLogout) + http.HandleFunc("/auth/failed", authFailed) + http.HandleFunc("/", home) + + // Using port 8080 + port := ":8080" + srv := &http.Server{Addr: port} + + // Launch the HTTP server + go func() { + + log.Printf("An instance of application '%s' has been started on port %s :)", os.Getenv("CE_APP"), port) + + if err := srv.ListenAndServe(); err != http.ErrServerClosed { + log.Fatalf("Failed to start server: %v", err) + } + }() + + // Add a SIGTERM listener to properly shutdown the app + <-signals + log.Println("Shutting down server") + if err := srv.Shutdown(ctx); err != nil { + log.Fatalf("Failed to shutdown server: %v", err) + } + log.Println("Shutdown done") + +} + +// Helper function that converts any object into a JSON string representation +func toJSONString(obj interface{}) string { + if obj == nil { + return "" + } + + bytes, err := json.Marshal(&obj) + if err != nil { + return "marshal error: " + err.Error() + } + + return string(bytes) +} diff --git a/oidc/go/build b/oidc/go/build new file mode 100755 index 000000000..47c9926b4 --- /dev/null +++ b/oidc/go/build @@ -0,0 +1,19 @@ +#!/bin/bash + +# Env Vars: +# REGISTRY: name of the image registry/namespace to store the images +# NOCACHE: set this to "--no-cache" to turn off the Docker build cache +# +# NOTE: to run this you MUST set the REGISTRY environment variable to +# your own image registry/namespace otherwise the `docker push` commands +# will fail due to an auth failure. Which means, you also need to be logged +# into that registry before you run it. + +set -ex +export REGISTRY=${REGISTRY:-icr.io/codeengine} + +# Build the image +docker build ${NOCACHE} -t ${REGISTRY}/oidc-go . --platform linux/amd64 + +# And push it +docker push ${REGISTRY}/oidc-go diff --git a/oidc/go/go.mod b/oidc/go/go.mod new file mode 100644 index 000000000..b394bc96b --- /dev/null +++ b/oidc/go/go.mod @@ -0,0 +1,8 @@ +module github.ibm.com/CodeEngine/oidc + +go 1.22.5 + +require ( + golang.org/x/net v0.35.0 + golang.org/x/oauth2 v0.26.0 +) diff --git a/oidc/go/go.sum b/oidc/go/go.sum new file mode 100644 index 000000000..425f5a375 --- /dev/null +++ b/oidc/go/go.sum @@ -0,0 +1,6 @@ +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= diff --git a/oidc/go/static/auth-failed.html b/oidc/go/static/auth-failed.html new file mode 100644 index 000000000..e7454c439 --- /dev/null +++ b/oidc/go/static/auth-failed.html @@ -0,0 +1,29 @@ +{{ define "authFailed" }} + + + + + + + + {{.Title}} + + +

Authentication failed!

+
OIDC configuration properties:
+
+ + ClientId={{.ClientId}} +
+ AuthorizationEndpoint={{.ProviderAuthorizationEndpoint}} +
+ TokenEndpoint={{.ProviderTokenEndpoint}} +
+ UserInfoEndpoint={{.ProviderUserInfoEndpoint}} +
+
+
+ Login + + +{{ end }} \ No newline at end of file diff --git a/oidc/go/static/auth-logout.html b/oidc/go/static/auth-logout.html new file mode 100644 index 000000000..ec316f93d --- /dev/null +++ b/oidc/go/static/auth-logout.html @@ -0,0 +1,16 @@ +{{ define "authLogout" }} + + + + + + + + {{.Title}} + + +

Log out done!

+ Login + + +{{ end }} diff --git a/oidc/go/static/css/style.css b/oidc/go/static/css/style.css new file mode 100644 index 000000000..71632b6b9 --- /dev/null +++ b/oidc/go/static/css/style.css @@ -0,0 +1,13 @@ +/* style.css */ +body,html { + background-color: #f4f4f4; + margin: 0 auto; + font-family: "IBM Plex Sans", sans-serif; + padding: 1rem; +} + +pre { + text-wrap: wrap; + word-break: break-word; + width: 95%; +} diff --git a/oidc/go/static/home.html b/oidc/go/static/home.html new file mode 100644 index 000000000..68f1e294b --- /dev/null +++ b/oidc/go/static/home.html @@ -0,0 +1,39 @@ +{{ define "home" }} + + + + + + + + {{.Title}} + + + {{if .User.Token}} +

Your are authenticated!

+
OIDC configuration properties:
+
+ + ClientId={{.ClientId}} +
+ AuthorizationEndpoint={{.ProviderAuthorizationEndpoint}} +
+ TokenEndpoint={{.ProviderTokenEndpoint}} +
+ UserInfoEndpoint={{.ProviderUserInfoEndpoint}} +
+
+
+
Access Token:
{{.User.Token}}
+
+
User Profile:
{{.User.Profile}}
+
+ Logout + {{else}} + Login + {{end}} + + + + +{{ end }} \ No newline at end of file diff --git a/oidc/go/static/images/favicon.ico b/oidc/go/static/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8f688bed87fc160ec873ed29f827dab6ef62b71c GIT binary patch literal 2247 zcmV;&2srnNP)&H!#vjgP;#EPD z`C&5ANgRF%Iywquq7e;_8m*5J#TEsomPcP;q4ojN_TKi~d-h&yew1R7i*0Y)flOG* zP4+ow?Y+Nm@3p_Z_P)SbJB5kR4c&FWTh>)~`*?Pv#;f7wXbrQ)Lk!$Io&=onYB<@Z z2pTMky1zEnb8QhJ>8QhUL%XvL$yYziXP_4>7AJeB*m8`k&)QVB4iL zUh})+Z%{DjmW5yJzxIcGQ+0Jows@PF-W#gjNdDxzoN)dEn@m2t*}Y)<aH;zT$qa{G|KR8&aUG`3KyP@HZ!uN-&c;UYePS>vfi_Xrv ziM@D2X{>{?FXi_6UshE_z4UU~6!PMJRJ-aT5}FC(xCU>F_w;;OQ(o>5)#nXIci#fd zKXOF>X>;y*ohK4_zHC20O%&_hUweJ=Uq_R%Uf$4B z0zWw!jP_vr*UeQG6=vY`m96^<1A>Q@tM1joKtTND1}&+_wiL|$Dy@v}Us3Q$)84$L zHr~BL7@az|;E6sXO9#G}ymoLpiMv6>$@lxbJ4OW_0ElF3>GXPVa5o$Aj!c{wJV9Vn zSm{oh{DAoQ(vSBodwXKpZG*w5-v?sEO?OUwF_oZ&rYC-@kzoH$x=}PG-DeQUj`|7j zzCAY6OhH-hkyV7Q^#l{ihE6Vp4|Sh0cF#{}g2cB_bvZBy~wGEkSkek?L~#PC{d%7GB>} zsIqi9gnz;!5BRZ|JO*L+x7`gKM#aL%(wo*+$ksDQKVz+E}DnZTdD`OqY#Y_TS` zNu*lpmz8x7nP%74W_u>If;6onl0z!s{PyG7Ga83SASMpb>=b2%wTcAS6Nx?4o_{A| zWJU^p>gGd{u$2Ng~XW`=T*_@(I3o4v(~f~O$Z*H zHX(09DCN2ZY{?O;w`Kw!F;S7Ogd?eNcipG!+eQi=0JhGX`6@F#C6+Ev_Q->$mQfMP zU(<78Ku69KL;7f>A>lv=#ZtFnyDy{AFRsyEdxzh6Ykm)6G0t@Hhrexp&RVJxrn?vK zsJ}2RfxO3iFBL20VpMOJ&D)P(m;3CKW<#AU!H$f2ez%cZwzj13y)=5HR#j191nL>K zlxJA4IEg^`kG08CpPBH3+ z1=WWlB0|Rr5Ifhir70%ez!k_OiEqA7P$gCm@c`aUTO1tjn}_7oaPzu z5f;`Uc9PjMO8Wtcm3e?BB9%7Z=*h|HhSz0rxLyM|tuw(wQTP@Bf794kBdVVj)mnor zPMVzbn-Qgu=;*~asnfm=*x(9Iafe5`VflguRsn!zy;m%`!ZhcUHJ^UT2WstD-DG5# zPt6f=udr5qw6|hvXN-vZWz@$W5$d#-mI87LfHB6VNu- z??O}seub-s(Dc(1M$wIJ*NV=%+a!8F$UdiN%gL#P###WqV64U}nS?XQqR+Qoq!3)k zJ|_kG!J0wmhwliirQHxb37{Mn&ta7mp9$b+K6iL20^uzJXR7s*_tgKiaQhFO2ZXQ^ zk3jGT0AE4yI2dnYJ$e6Y;ODOGnJb#_B}e~;6nd3S+|zVd(F?;nk=-pT0Xz!AQ~=u{ zsDi?7FcQ!JlqC%SN<(W4=ws8Vaja~i6^_5x8i3|O<$+s=L3POzX$mWiC=?cxrYm(; zwn8%gks$f|mX(vXjqFgrN=jg{3cyVul))L~tpHL0>3+~O@Jxfsnf^ooljv4jeJBpE zZ>`?*I@X^0+Hq)2|E1)>K^ZL01rRx=MTBD`snx(Jr<;jmql{y|$KP{*8H3xfP>%B8 zpcTRm?e}L7>%W4b35XCL?GGFrKH0xCU7fN_BflPb!BSq{3LA?NEW literal 0 HcmV?d00001 From 9820e6a11a8895c363862019ecd46b3308db1d1a Mon Sep 17 00:00:00 2001 From: Enrico Regge Date: Fri, 28 Mar 2025 01:03:55 +0100 Subject: [PATCH 2/9] Added a node.js example --- oidc/README.md | 10 +- oidc/go/build | 4 +- oidc/node/Dockerfile | 14 + oidc/node/build | 19 + oidc/node/index.mjs | 183 ++++++ oidc/node/package-lock.json | 975 ++++++++++++++++++++++++++++ oidc/node/package.json | 17 + oidc/node/public/css/style.css | 13 + oidc/node/public/images/favicon.ico | Bin 0 -> 2247 bytes oidc/node/views/authfailed.ejs | 29 + oidc/node/views/index.ejs | 37 ++ 11 files changed, 1296 insertions(+), 5 deletions(-) create mode 100644 oidc/node/Dockerfile create mode 100755 oidc/node/build create mode 100644 oidc/node/index.mjs create mode 100644 oidc/node/package-lock.json create mode 100644 oidc/node/package.json create mode 100644 oidc/node/public/css/style.css create mode 100644 oidc/node/public/images/favicon.ico create mode 100644 oidc/node/views/authfailed.ejs create mode 100644 oidc/node/views/index.ejs diff --git a/oidc/README.md b/oidc/README.md index fcb092534..2697f6715 100644 --- a/oidc/README.md +++ b/oidc/README.md @@ -26,12 +26,16 @@ ibmcloud ce secret create --name oidc-credentials --from-env-file .env * Create the application ``` +ENCRYPTION_KEY=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64 | tr -d -- '\n' | tr -- '+/' '-_' ; echo) LANGUAGE=go -cd $LANGUAGE -ibmcloud ce app create --name oidc-sample-$LANGUAGE --src . \ +ibmcloud ce app create --name oidc-sample-$LANGUAGE \ + --src "." \ + --build-context-dir "$LANGUAGE" \ --cpu 0.125 \ --memory 0.25G \ - --env-from-secret oidc-credentials + --env-from-secret oidc-credentials \ + --env COOKIE_SIGNING_PASSPHRASE=$ENCRYPTION_KEY \ + --env OIDC_REDIRECT_URL=https://oidc-sample-$LANGUAGE.1ryejitws058.eu-es.codeengine.appdomain.cloud/auth/callback OIDC_REDIRECT_URL=$(ibmcloud ce app get -n oidc-sample-$LANGUAGE --output url) ibmcloud ce app update --name oidc-sample-$LANGUAGE --env OIDC_REDIRECT_URL=$OIDC_REDIRECT_URL/auth/callback diff --git a/oidc/go/build b/oidc/go/build index 47c9926b4..c8b4bceb8 100755 --- a/oidc/go/build +++ b/oidc/go/build @@ -13,7 +13,7 @@ set -ex export REGISTRY=${REGISTRY:-icr.io/codeengine} # Build the image -docker build ${NOCACHE} -t ${REGISTRY}/oidc-go . --platform linux/amd64 +docker build ${NOCACHE} -t ${REGISTRY}/oidc/go . --platform linux/amd64 # And push it -docker push ${REGISTRY}/oidc-go +docker push ${REGISTRY}/oidc/go diff --git a/oidc/node/Dockerfile b/oidc/node/Dockerfile new file mode 100644 index 000000000..1f9de3d4e --- /dev/null +++ b/oidc/node/Dockerfile @@ -0,0 +1,14 @@ +FROM registry.access.redhat.com/ubi9/nodejs-22:latest AS build-env +WORKDIR /app +COPY index.mjs . +COPY package.json . +RUN npm install + +# Use a small distroless image for as runtime image +FROM gcr.io/distroless/nodejs22-debian12 +COPY --from=build-env /app /app +WORKDIR /app +COPY public/ public/ +COPY views/ views/ +EXPOSE 8080 +CMD ["index.mjs"] \ No newline at end of file diff --git a/oidc/node/build b/oidc/node/build new file mode 100755 index 000000000..dbaa4c486 --- /dev/null +++ b/oidc/node/build @@ -0,0 +1,19 @@ +#!/bin/bash + +# Env Vars: +# REGISTRY: name of the image registry/namespace to store the images +# NOCACHE: set this to "--no-cache" to turn off the Docker build cache +# +# NOTE: to run this you MUST set the REGISTRY environment variable to +# your own image registry/namespace otherwise the `docker push` commands +# will fail due to an auth failure. Which means, you also need to be logged +# into that registry before you run it. + +set -ex +export REGISTRY=${REGISTRY:-icr.io/codeengine} + +# Build the image +docker build ${NOCACHE} -t ${REGISTRY}/oidc/node . --platform linux/amd64 + +# And push it +docker push ${REGISTRY}/oidc/node diff --git a/oidc/node/index.mjs b/oidc/node/index.mjs new file mode 100644 index 000000000..e42d20e53 --- /dev/null +++ b/oidc/node/index.mjs @@ -0,0 +1,183 @@ +// require expressjs +import express from "express"; +import fs from 'fs'; + +import cookieParser from "cookie-parser"; +import path from "path"; +import { fileURLToPath } from "url"; +const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file +const __dirname = path.dirname(__filename); // get the name of the directory + +const SESSION_COOKIE = "session_token"; + +const requiredEnvVars = [ + "OIDC_CLIENT_ID", + "OIDC_CLIENT_SECRET", + "OIDC_PROVIDER_AUTHORIZATION_ENDPOINT", + "OIDC_PROVIDER_TOKEN_ENDPOINT", + "OIDC_PROVIDER_USERINFO_ENDPOINT", + "OIDC_REDIRECT_URL", + "COOKIE_SIGNING_PASSPHRASE", +]; + +requiredEnvVars.forEach((envVarName) => { + if (!process.env[envVarName]) { + console.log(`Missing '${envVarName}' environment variable`); + process.exit(1); + } +}); +console.log('Files in root:') +fs.readdirSync(path.join(__dirname, "")).forEach(file => { + console.log(file); +}); +// check whether the auth cookie is set +async function checkAuth(req, res, next) { + console.log(`performing auth check for '${req.url}'`); + + console.log("Cookies: ", req.cookies); + const sessionToken = req.cookies[SESSION_COOKIE]; + + if (!sessionToken) { + console.log(`session cookie '${SESSION_COOKIE}' not found`); + return res.redirect("/auth/login"); + } + + const opts = { + method: "GET", + headers: { + Authorization: `Bearer ${sessionToken}`, + }, + }; + console.log( + `Fetching user data from '${process.env.OIDC_PROVIDER_USERINFO_ENDPOINT}' with '${JSON.stringify(opts)}'...` + ); + // exchange authorization code for access token & id_token + const response = await fetch(process.env.OIDC_PROVIDER_USERINFO_ENDPOINT, opts); + + console.log(`response.ok: '${response.ok}', response.status: '${response.status}'`); + if (!response.ok) { + const errorResponse = await response.text(); + console.log(`errorResponse: '${errorResponse}'`); + return res.redirect("/auth/failed"); + } + + const user_data = await response.json(); + console.log(`user_data: '${JSON.stringify(user_data)}'`); + + // setting user into the request context + req.user = user_data; + + next(); +} + +const app = express(); +app.use(express.json()); +app.use(cookieParser()); + +// Define a view engine +app.set("view engine", "ejs"); +app.set("views", path.join(__dirname, "views")); + +// use router to bundle all routes to / +const router = express.Router(); + +app.use("/", router); + +router.get("/auth/callback", async (req, res) => { + console.log(`handling /auth/callback`); + + const { code } = req.query; + const data = { + code, + redirect_uri: process.env.OIDC_REDIRECT_URL, + grant_type: "authorization_code", + }; + + console.log(data); + + // exchange authorization code for access token & id_token + const response = await fetch(`${process.env.OIDC_PROVIDER_TOKEN_ENDPOINT}?${new URLSearchParams(data).toString()}`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + Authorization: + "Basic " + Buffer.from(process.env.OIDC_CLIENT_ID + ":" + process.env.OIDC_CLIENT_SECRET).toString("base64"), + }, + }); + + console.log(`response.ok: '${response.ok}', response.status: '${response.status}'`); + if (!response.ok) { + const errorResponse = await response.text(); + console.log(`errorResponse: '${errorResponse}'`); + return res.redirect("/auth/failed"); + } + + const access_token_data = await response.json(); + console.log(`access_token_data: '${JSON.stringify(access_token_data)}'`); + + console.log("Setting session cookie."); + res.cookie(SESSION_COOKIE, access_token_data.access_token, { + maxAge: 1000 * access_token_data.expires_in, + httpOnly: true, + path: "/", + secure: true, + }); + + // redirect to the home route + return res.redirect("/"); +}); + +router.get("/auth/login", (req, res) => { + console.log(`handling /auth/login for '${req.url}'`); + console.log(`baseUrl: '${req.baseUrl}'`); + + // redirect to the configured OIDC provider + res.redirect( + `${process.env.OIDC_PROVIDER_AUTHORIZATION_ENDPOINT}?client_id=${ + process.env.OIDC_CLIENT_ID + }&redirect_uri=${encodeURIComponent( + process.env.OIDC_REDIRECT_URL + )}&response_type=code&scope=openid+profile&state=state` + ); +}); + +const viewParams = { + pageTitle: "OIDC sample - IBM Cloud Code Engine", + clientId: process.env.OIDC_CLIENT_ID, + providerAuthorizationEndpoint: process.env.OIDC_PROVIDER_AUTHORIZATION_ENDPOINT, + providerTokenEndpoint: process.env.OIDC_PROVIDER_TOKEN_ENDPOINT, + providerUserInfoEndpoint: process.env.OIDC_PROVIDER_USERINFO_ENDPOINT, +}; + +// route that renders an auth failed page +router.get("/auth/failed", (req, res) => { + console.log(`handling /auth/failed for '${req.url}'`); + res.status(401); + res.render("authfailed", viewParams); +}); + +// get on root route +router.get("/", checkAuth, (req, res) => { + console.log(`handling / for '${req.url}'`); + res.render("index", { + ...viewParams, + user: { token: req.cookies[SESSION_COOKIE], profile: JSON.stringify(req.user) }, + }); +}); + +// serve static files +app.use("/public", express.static("public")); + +// start server +const port = process.env.PORT || 8080; +const server = app.listen(port, () => { + console.log(`Server is up and running on port ${port}!`); +}); + +process.on("SIGTERM", () => { + console.info("SIGTERM signal received."); + server.close(() => { + console.log("Http server closed."); + }); +}); diff --git a/oidc/node/package-lock.json b/oidc/node/package-lock.json new file mode 100644 index 000000000..f12a9eca9 --- /dev/null +++ b/oidc/node/package-lock.json @@ -0,0 +1,975 @@ +{ + "name": "code-engine-nodejs", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "code-engine-nodejs", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "cookie-parser": "^1.4.7", + "cookies": "^0.9.1", + "ejs": "^3.1.10", + "express": "^4.21.2" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cookies": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", + "dependencies": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "dependencies": { + "tsscmp": "1.0.6" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "engines": { + "node": ">=0.6.x" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/oidc/node/package.json b/oidc/node/package.json new file mode 100644 index 000000000..f1dda7c7b --- /dev/null +++ b/oidc/node/package.json @@ -0,0 +1,17 @@ +{ + "name": "code-engine-nodejs", + "version": "1.0.0", + "description": "Simple nodejs server hosted on IBM cloud engine", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "cookie-parser": "^1.4.7", + "ejs": "^3.1.10", + "express": "^4.21.2" + } +} diff --git a/oidc/node/public/css/style.css b/oidc/node/public/css/style.css new file mode 100644 index 000000000..71632b6b9 --- /dev/null +++ b/oidc/node/public/css/style.css @@ -0,0 +1,13 @@ +/* style.css */ +body,html { + background-color: #f4f4f4; + margin: 0 auto; + font-family: "IBM Plex Sans", sans-serif; + padding: 1rem; +} + +pre { + text-wrap: wrap; + word-break: break-word; + width: 95%; +} diff --git a/oidc/node/public/images/favicon.ico b/oidc/node/public/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8f688bed87fc160ec873ed29f827dab6ef62b71c GIT binary patch literal 2247 zcmV;&2srnNP)&H!#vjgP;#EPD z`C&5ANgRF%Iywquq7e;_8m*5J#TEsomPcP;q4ojN_TKi~d-h&yew1R7i*0Y)flOG* zP4+ow?Y+Nm@3p_Z_P)SbJB5kR4c&FWTh>)~`*?Pv#;f7wXbrQ)Lk!$Io&=onYB<@Z z2pTMky1zEnb8QhJ>8QhUL%XvL$yYziXP_4>7AJeB*m8`k&)QVB4iL zUh})+Z%{DjmW5yJzxIcGQ+0Jows@PF-W#gjNdDxzoN)dEn@m2t*}Y)<aH;zT$qa{G|KR8&aUG`3KyP@HZ!uN-&c;UYePS>vfi_Xrv ziM@D2X{>{?FXi_6UshE_z4UU~6!PMJRJ-aT5}FC(xCU>F_w;;OQ(o>5)#nXIci#fd zKXOF>X>;y*ohK4_zHC20O%&_hUweJ=Uq_R%Uf$4B z0zWw!jP_vr*UeQG6=vY`m96^<1A>Q@tM1joKtTND1}&+_wiL|$Dy@v}Us3Q$)84$L zHr~BL7@az|;E6sXO9#G}ymoLpiMv6>$@lxbJ4OW_0ElF3>GXPVa5o$Aj!c{wJV9Vn zSm{oh{DAoQ(vSBodwXKpZG*w5-v?sEO?OUwF_oZ&rYC-@kzoH$x=}PG-DeQUj`|7j zzCAY6OhH-hkyV7Q^#l{ihE6Vp4|Sh0cF#{}g2cB_bvZBy~wGEkSkek?L~#PC{d%7GB>} zsIqi9gnz;!5BRZ|JO*L+x7`gKM#aL%(wo*+$ksDQKVz+E}DnZTdD`OqY#Y_TS` zNu*lpmz8x7nP%74W_u>If;6onl0z!s{PyG7Ga83SASMpb>=b2%wTcAS6Nx?4o_{A| zWJU^p>gGd{u$2Ng~XW`=T*_@(I3o4v(~f~O$Z*H zHX(09DCN2ZY{?O;w`Kw!F;S7Ogd?eNcipG!+eQi=0JhGX`6@F#C6+Ev_Q->$mQfMP zU(<78Ku69KL;7f>A>lv=#ZtFnyDy{AFRsyEdxzh6Ykm)6G0t@Hhrexp&RVJxrn?vK zsJ}2RfxO3iFBL20VpMOJ&D)P(m;3CKW<#AU!H$f2ez%cZwzj13y)=5HR#j191nL>K zlxJA4IEg^`kG08CpPBH3+ z1=WWlB0|Rr5Ifhir70%ez!k_OiEqA7P$gCm@c`aUTO1tjn}_7oaPzu z5f;`Uc9PjMO8Wtcm3e?BB9%7Z=*h|HhSz0rxLyM|tuw(wQTP@Bf794kBdVVj)mnor zPMVzbn-Qgu=;*~asnfm=*x(9Iafe5`VflguRsn!zy;m%`!ZhcUHJ^UT2WstD-DG5# zPt6f=udr5qw6|hvXN-vZWz@$W5$d#-mI87LfHB6VNu- z??O}seub-s(Dc(1M$wIJ*NV=%+a!8F$UdiN%gL#P###WqV64U}nS?XQqR+Qoq!3)k zJ|_kG!J0wmhwliirQHxb37{Mn&ta7mp9$b+K6iL20^uzJXR7s*_tgKiaQhFO2ZXQ^ zk3jGT0AE4yI2dnYJ$e6Y;ODOGnJb#_B}e~;6nd3S+|zVd(F?;nk=-pT0Xz!AQ~=u{ zsDi?7FcQ!JlqC%SN<(W4=ws8Vaja~i6^_5x8i3|O<$+s=L3POzX$mWiC=?cxrYm(; zwn8%gks$f|mX(vXjqFgrN=jg{3cyVul))L~tpHL0>3+~O@Jxfsnf^ooljv4jeJBpE zZ>`?*I@X^0+Hq)2|E1)>K^ZL01rRx=MTBD`snx(Jr<;jmql{y|$KP{*8H3xfP>%B8 zpcTRm?e}L7>%W4b35XCL?GGFrKH0xCU7fN_BflPb!BSq{3LA?NEW literal 0 HcmV?d00001 diff --git a/oidc/node/views/authfailed.ejs b/oidc/node/views/authfailed.ejs new file mode 100644 index 000000000..aba3189a1 --- /dev/null +++ b/oidc/node/views/authfailed.ejs @@ -0,0 +1,29 @@ + + + + + + + + <%= pageTitle %> + + +

Authentication failed!

+
OIDC configuration properties:
+
+ + ClientId=<%= clientId %> +
+ AuthorizationEndpoint=<%= providerAuthorizationEndpoint %> +
+ TokenEndpoint=<%= providerTokenEndpoint %> +
+ UserInfoEndpoint=<%= providerUserInfoEndpoint %> +
+
+
+ Login + + + + \ No newline at end of file diff --git a/oidc/node/views/index.ejs b/oidc/node/views/index.ejs new file mode 100644 index 000000000..e15f8dd0f --- /dev/null +++ b/oidc/node/views/index.ejs @@ -0,0 +1,37 @@ + + + + + + + + <%= pageTitle %> + + + <% if(typeof user == 'object' && user?.token){ %> +

Your are authenticated!

+
OIDC configuration properties:
+
+ + ClientId=<%= clientId %> +
+ AuthorizationEndpoint=<%= providerAuthorizationEndpoint %> +
+ TokenEndpoint=<%= providerTokenEndpoint %> +
+ UserInfoEndpoint=<%= providerUserInfoEndpoint %> +
+
+
+
Access Token:
<%= user.token %>
+
+
User Profile:
<%= user.profile %>
+
+ Logout + <% } else{ %> + Login + <% } %> + + + + \ No newline at end of file From af41c931d58bc711c914f003b2a6c02206b99dc4 Mon Sep 17 00:00:00 2001 From: Enrico Regge Date: Fri, 4 Apr 2025 12:07:40 +0200 Subject: [PATCH 3/9] Cleaned up debug output --- oidc/node/index.mjs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/oidc/node/index.mjs b/oidc/node/index.mjs index e42d20e53..d9d5f7cae 100644 --- a/oidc/node/index.mjs +++ b/oidc/node/index.mjs @@ -26,10 +26,7 @@ requiredEnvVars.forEach((envVarName) => { process.exit(1); } }); -console.log('Files in root:') -fs.readdirSync(path.join(__dirname, "")).forEach(file => { - console.log(file); -}); + // check whether the auth cookie is set async function checkAuth(req, res, next) { console.log(`performing auth check for '${req.url}'`); From f714dfdb358504ae4a4a1789017496013ea5a5c2 Mon Sep 17 00:00:00 2001 From: Enrico Regge Date: Fri, 25 Apr 2025 23:02:28 +0200 Subject: [PATCH 4/9] Added encryption --- oidc/node/index.mjs | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/oidc/node/index.mjs b/oidc/node/index.mjs index d9d5f7cae..d59ec260c 100644 --- a/oidc/node/index.mjs +++ b/oidc/node/index.mjs @@ -27,18 +27,36 @@ requiredEnvVars.forEach((envVarName) => { } }); +function encrypt(plaintext, key, iv) { + const cipher = crypto.createCipheriv("aes-256-gcm", key, iv); + let ciphertext = cipher.update(plaintext, "utf8", "base64"); + ciphertext += cipher.final("base64"); + return ciphertext; +} + +function decrypt(ciphertext, key, iv) { + const decipher = crypto.createDecipheriv("aes-256-gcm", Buffer.from(key, "base64"), Buffer.from(iv, "base64")); + let plaintext = decipher.update(ciphertext, "base64", "utf8"); + plaintext += decipher.final("utf8"); + + return plaintext; +} + // check whether the auth cookie is set async function checkAuth(req, res, next) { console.log(`performing auth check for '${req.url}'`); console.log("Cookies: ", req.cookies); - const sessionToken = req.cookies[SESSION_COOKIE]; + const encryptedSessionToken = req.cookies[SESSION_COOKIE]; - if (!sessionToken) { + if (!encryptedSessionToken) { console.log(`session cookie '${SESSION_COOKIE}' not found`); return res.redirect("/auth/login"); } + // decrypt session token + const sessionToken = decrypt(encryptedSessionToken, process.env.COOKIE_SIGNING_PASSPHRASE, IV); + const opts = { method: "GET", headers: { @@ -113,8 +131,11 @@ router.get("/auth/callback", async (req, res) => { const access_token_data = await response.json(); console.log(`access_token_data: '${JSON.stringify(access_token_data)}'`); + // encrypt the access token + const sessionCookieValue = encrypt(access_token_data.access_token, process.env.COOKIE_SIGNING_PASSPHRASE, IV); + console.log("Setting session cookie."); - res.cookie(SESSION_COOKIE, access_token_data.access_token, { + res.cookie(SESSION_COOKIE, sessionCookieValue, { maxAge: 1000 * access_token_data.expires_in, httpOnly: true, path: "/", From ffd2c7d9acf45c37eef8cb9fa24bfc970b21d89c Mon Sep 17 00:00:00 2001 From: Enrico Regge Date: Tue, 25 Nov 2025 18:56:02 +0100 Subject: [PATCH 5/9] Updated the auth-oidc sample --- auth-oidc/.gitignore | 1 + auth-oidc/README.md | 53 ++ auth-oidc/node/Dockerfile | 14 + auth-oidc/node/build | 19 + auth-oidc/node/index.mjs | 248 ++++++ auth-oidc/node/package-lock.json | 975 +++++++++++++++++++++++ auth-oidc/node/package.json | 17 + auth-oidc/node/public/css/style.css | 13 + auth-oidc/node/public/images/favicon.ico | Bin 0 -> 2247 bytes auth-oidc/node/views/authfailed.ejs | 29 + auth-oidc/node/views/index.ejs | 37 + 11 files changed, 1406 insertions(+) create mode 100644 auth-oidc/.gitignore create mode 100644 auth-oidc/README.md create mode 100644 auth-oidc/node/Dockerfile create mode 100755 auth-oidc/node/build create mode 100644 auth-oidc/node/index.mjs create mode 100644 auth-oidc/node/package-lock.json create mode 100644 auth-oidc/node/package.json create mode 100644 auth-oidc/node/public/css/style.css create mode 100644 auth-oidc/node/public/images/favicon.ico create mode 100644 auth-oidc/node/views/authfailed.ejs create mode 100644 auth-oidc/node/views/index.ejs diff --git a/auth-oidc/.gitignore b/auth-oidc/.gitignore new file mode 100644 index 000000000..2eea525d8 --- /dev/null +++ b/auth-oidc/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/auth-oidc/README.md b/auth-oidc/README.md new file mode 100644 index 000000000..330885c45 --- /dev/null +++ b/auth-oidc/README.md @@ -0,0 +1,53 @@ +# OAuth 2.0 / OIDC + +A pretty simple golang application that, in its most basic form, will +return "Hello World" back to the caller. + +Check the source code for all of the things you can make it do either via +environment variables or query parameters. This is good for testing the +system to see how it reacts - for example, when the app crashes. + +Note: we added some extra logic to this so I can also be used as a batch job +but you can ignore that if all you care about is the App side of things. + + +``` +OIDC_CLIENT_ID= > .env +OIDC_CLIENT_SECRET= >> .env +OIDC_PROVIDER_AUTHORIZATION_ENDPOINT= >> .env +OIDC_PROVIDER_TOKEN_ENDPOINT= >> .env +OIDC_PROVIDER_USERINFO_ENDPOINT= >> .env +``` + +* Create the secret +``` +ibmcloud ce secret create --name oidc-credentials --from-env-file .env +``` + +* Create the application +``` +ENCRYPTION_KEY=$(openssl rand -base64 32) +LANGUAGE=go +ibmcloud ce app create --name oidc-sample-$LANGUAGE \ + --src "." \ + --build-context-dir "$LANGUAGE" \ + --cpu 0.125 \ + --memory 0.25G \ + --env-from-secret oidc-credentials \ + --env COOKIE_ENCRYPTION_KEY=$ENCRYPTION_KEY \ + --env OIDC_REDIRECT_URL=https://oidc-sample-$LANGUAGE.1ryejitws058.eu-es.codeengine.appdomain.cloud/auth/callback + +OIDC_REDIRECT_URL=$(ibmcloud ce app get -n oidc-sample-$LANGUAGE --output url) +ibmcloud ce app update --name oidc-sample-$LANGUAGE --env OIDC_REDIRECT_URL=$OIDC_REDIRECT_URL/auth/callback +cd .. +``` + +- - - + +As noted in [the main README](../README.md), this sample has two pieces: + +- a `build` script which will build the container image(s) used +- a `run` script which deploys resources that use those images + +The main purpose of this example is the `run` script, but the `build` +script is included for complete educational (and reuse) purposes. diff --git a/auth-oidc/node/Dockerfile b/auth-oidc/node/Dockerfile new file mode 100644 index 000000000..1f9de3d4e --- /dev/null +++ b/auth-oidc/node/Dockerfile @@ -0,0 +1,14 @@ +FROM registry.access.redhat.com/ubi9/nodejs-22:latest AS build-env +WORKDIR /app +COPY index.mjs . +COPY package.json . +RUN npm install + +# Use a small distroless image for as runtime image +FROM gcr.io/distroless/nodejs22-debian12 +COPY --from=build-env /app /app +WORKDIR /app +COPY public/ public/ +COPY views/ views/ +EXPOSE 8080 +CMD ["index.mjs"] \ No newline at end of file diff --git a/auth-oidc/node/build b/auth-oidc/node/build new file mode 100755 index 000000000..dbaa4c486 --- /dev/null +++ b/auth-oidc/node/build @@ -0,0 +1,19 @@ +#!/bin/bash + +# Env Vars: +# REGISTRY: name of the image registry/namespace to store the images +# NOCACHE: set this to "--no-cache" to turn off the Docker build cache +# +# NOTE: to run this you MUST set the REGISTRY environment variable to +# your own image registry/namespace otherwise the `docker push` commands +# will fail due to an auth failure. Which means, you also need to be logged +# into that registry before you run it. + +set -ex +export REGISTRY=${REGISTRY:-icr.io/codeengine} + +# Build the image +docker build ${NOCACHE} -t ${REGISTRY}/oidc/node . --platform linux/amd64 + +# And push it +docker push ${REGISTRY}/oidc/node diff --git a/auth-oidc/node/index.mjs b/auth-oidc/node/index.mjs new file mode 100644 index 000000000..d7bf8bbf5 --- /dev/null +++ b/auth-oidc/node/index.mjs @@ -0,0 +1,248 @@ +import express from "express"; +import crypto from 'crypto'; +import cookieParser from "cookie-parser"; +import path from "path"; +import { fileURLToPath } from "url"; +const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file +const __dirname = path.dirname(__filename); // get the name of the directory + +const requiredEnvVars = [ + "OIDC_CLIENT_ID", + "OIDC_CLIENT_SECRET", + "OIDC_PROVIDER_AUTHORIZATION_ENDPOINT", + "OIDC_PROVIDER_TOKEN_ENDPOINT", + "OIDC_PROVIDER_USERINFO_ENDPOINT", + "OIDC_REDIRECT_URL", + "COOKIE_ENCRYPTION_KEY", +]; + +let missingEnvVars = []; +requiredEnvVars.forEach((envVarName) => { + if (!process.env[envVarName]) { + console.log(`Missing '${envVarName}' environment variable`); + missingEnvVars.push(envVarName); + } +}); + +if (missingEnvVars.length > 0) { + console.log(`Aborting due to missing env vars '${JSON.stringify(missingEnvVars)}'`); +} + +const SESSION_COOKIE = process.env.COOKIE_NAME || "session_token"; +const ENCRYPTION_KEY = Buffer.from(process.env.COOKIE_ENCRYPTION_KEY, "base64"); +const ENCRYPTION_IV = crypto.randomBytes(16); +const ENCRYPTION_ALGORITHM = "aes-256-cbc"; + +// check whether the KEY has got 32 bytes (256-bit) +if (process.env.COOKIE_ENCRYPTION_KEY && ENCRYPTION_KEY.length != 32) { + console.log( + `Environment variable 'COOKIE_ENCRYPTION_KEY' has wrong length. Current: ${ENCRYPTION_KEY.length}. Expected: 32` + ); + process.exit(1); +} + +// ================================================= +// HELPER FUNCTIONS +// ================================================= + +// helper function to encrypt a string using the given encryption key and iv +function encrypt(plaintext, key, iv) { + const cipher = crypto.createCipheriv(ENCRYPTION_ALGORITHM, key, iv); + let ciphertext = cipher.update(plaintext, "utf8", "base64"); + ciphertext += cipher.final("base64"); + return ciphertext; +} + +// helper function to decrypt a string using the given encryption key and iv +function decrypt(ciphertext, key, iv) { + const decipher = crypto.createDecipheriv(ENCRYPTION_ALGORITHM, key, iv); + let plaintext = decipher.update(ciphertext, "base64", "utf8"); + plaintext += decipher.final("utf8"); + return plaintext; +} + +// check whether the auth cookie is set +async function checkAuth(req, res, next) { + console.log(`performing auth check for '${req.url}'`); + + if (missingEnvVars.length > 0) { + console.log(`redirecting request to auth failed page'`); + return res.redirect("/auth/failed"); + } + + console.log("Cookies: ", req.cookies); + const encryptedSessionToken = req.cookies[SESSION_COOKIE]; + + if (!encryptedSessionToken) { + console.log(`session cookie '${SESSION_COOKIE}' not found`); + return res.redirect("/auth/login"); + } + + // Decrypt session token + let sessionToken; + try { + sessionToken = decrypt(encryptedSessionToken, ENCRYPTION_KEY, ENCRYPTION_IV); + } catch (err) { + console.log(`${fn} failed to decrypt existing sessionToken - cause: '${err.name}', reason: '${err.message}'`); + + // This error indicates that the encrypted string couldn't get decrypted using the encryption key + // maybe the cookie value has been encrypted with an old key + // full error: 'error:1C800064:Provider routines::bad decrypt' + if (err.message.includes("error:1C800064")) { + console.log(`${fn} enryption key has been changed. Deleting existing cookie`); + res.clearCookie(SESSION_COOKIE); + return sendJSONResponse(res, 400, { reason: "invalid_session" }); + } + + // error:1C80006B:Provider routines::wrong final block length + if (err.message.includes("error:1C80006B")) { + console.log(`${fn} enryption key has been changed. Deleting existing cookie`); + res.clearCookie(SESSION_COOKIE); + return res.status(401).redirect("/auth/failed?code=invalid_session"); + } + + // If the decrypt mechanism failed, return a 500 + // It is up to the client to trigger a login procedure + return res.status(500).redirect("/auth/failed?code=decryption_failed"); + } + + const opts = { + method: "GET", + headers: { + Authorization: `Bearer ${sessionToken}`, + }, + }; + + // exchange authorization code for access token & id_token + const response = await fetch(process.env.OIDC_PROVIDER_USERINFO_ENDPOINT, opts); + + console.log(`fetched user data from '${process.env.OIDC_PROVIDER_USERINFO_ENDPOINT}' response.ok: '${response.ok}', response.status: '${response.status}'`); + if (!response.ok) { + const errorResponse = await response.text(); + console.log(`errorResponse: '${errorResponse}'`); + return res.redirect("/auth/failed"); + } + + const user_data = await response.json(); + // console.log(`user_data: '${JSON.stringify(user_data)}'`); + + // setting user into the request context + req.user = user_data; + + next(); +} + +const app = express(); +app.use(express.json()); +app.use(cookieParser()); + +// Define a view engine +app.set("view engine", "ejs"); +app.set("views", path.join(__dirname, "views")); + +// use router to bundle all routes to / +const router = express.Router(); + +app.use("/", router); + +router.get("/auth/callback", async (req, res) => { + console.log(`handling /auth/callback`); + + const { code } = req.query; + const data = { + code, + redirect_uri: process.env.OIDC_REDIRECT_URL, + grant_type: "authorization_code", + }; + + // exchange authorization code for access token & id_token + const response = await fetch(`${process.env.OIDC_PROVIDER_TOKEN_ENDPOINT}?${new URLSearchParams(data).toString()}`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + Authorization: + "Basic " + Buffer.from(process.env.OIDC_CLIENT_ID + ":" + process.env.OIDC_CLIENT_SECRET).toString("base64"), + }, + }); + + console.log(`response.ok: '${response.ok}', response.status: '${response.status}'`); + if (!response.ok) { + const errorResponse = await response.text(); + console.log(`errorResponse: '${errorResponse}'`); + return res.redirect("/auth/failed"); + } + + const accessTokenData = await response.json(); + + // encrypt the access token + const sessionCookieValue = encrypt(accessTokenData.access_token, ENCRYPTION_KEY, ENCRYPTION_IV); + const maxAge = accessTokenData.expires_in ? 1000 * accessTokenData.expires_in : 600_000; // defaults to 10min + console.log( + `Setting session cookie '${SESSION_COOKIE}' (max age: '${maxAge}ms')` + ); + + res.cookie(SESSION_COOKIE, sessionCookieValue, { + maxAge, + httpOnly: true, + path: "/", + secure: true, + }); + + // redirect to the home route + return res.redirect("/"); +}); + +router.get("/auth/login", (req, res) => { + console.log(`handling /auth/login for '${req.url}'`); + console.log(`baseUrl: '${req.baseUrl}'`); + + // redirect to the configured OIDC provider + res.redirect( + `${process.env.OIDC_PROVIDER_AUTHORIZATION_ENDPOINT}?client_id=${ + process.env.OIDC_CLIENT_ID + }&redirect_uri=${encodeURIComponent( + process.env.OIDC_REDIRECT_URL + )}&response_type=code&scope=openid+profile&state=state` + ); +}); + +const viewParams = { + pageTitle: "OIDC sample - IBM Cloud Code Engine", + clientId: process.env.OIDC_CLIENT_ID, + providerAuthorizationEndpoint: process.env.OIDC_PROVIDER_AUTHORIZATION_ENDPOINT, + providerTokenEndpoint: process.env.OIDC_PROVIDER_TOKEN_ENDPOINT, + providerUserInfoEndpoint: process.env.OIDC_PROVIDER_USERINFO_ENDPOINT, +}; + +// route that renders an auth failed page +router.get("/auth/failed", (req, res) => { + console.log(`handling /auth/failed for '${req.url}'`); + res.status(401); + res.render("authfailed", viewParams); +}); + +// get on root route +router.get("/", checkAuth, (req, res) => { + console.log(`handling / for '${req.url}'`); + res.render("index", { + ...viewParams, + user: { token: req.cookies[SESSION_COOKIE], profile: JSON.stringify(req.user) }, + }); +}); + +// serve static files +app.use("/public", express.static("public")); + +// start server +const port = process.env.PORT || 8080; +const server = app.listen(port, () => { + console.log(`Server is up and running on port ${port}!`); +}); + +process.on("SIGTERM", () => { + console.info("SIGTERM signal received."); + server.close(() => { + console.log("Http server closed."); + }); +}); diff --git a/auth-oidc/node/package-lock.json b/auth-oidc/node/package-lock.json new file mode 100644 index 000000000..f12a9eca9 --- /dev/null +++ b/auth-oidc/node/package-lock.json @@ -0,0 +1,975 @@ +{ + "name": "code-engine-nodejs", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "code-engine-nodejs", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "cookie-parser": "^1.4.7", + "cookies": "^0.9.1", + "ejs": "^3.1.10", + "express": "^4.21.2" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cookies": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", + "dependencies": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "dependencies": { + "tsscmp": "1.0.6" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "engines": { + "node": ">=0.6.x" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/auth-oidc/node/package.json b/auth-oidc/node/package.json new file mode 100644 index 000000000..d2c2de096 --- /dev/null +++ b/auth-oidc/node/package.json @@ -0,0 +1,17 @@ +{ + "name": "code-engine-auth-oidc", + "version": "1.0.0", + "description": "Simple Node.js app on IBM Cloud Cloud Engine", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "cookie-parser": "^1.4.7", + "ejs": "^3.1.10", + "express": "^5.1.0" + } +} diff --git a/auth-oidc/node/public/css/style.css b/auth-oidc/node/public/css/style.css new file mode 100644 index 000000000..71632b6b9 --- /dev/null +++ b/auth-oidc/node/public/css/style.css @@ -0,0 +1,13 @@ +/* style.css */ +body,html { + background-color: #f4f4f4; + margin: 0 auto; + font-family: "IBM Plex Sans", sans-serif; + padding: 1rem; +} + +pre { + text-wrap: wrap; + word-break: break-word; + width: 95%; +} diff --git a/auth-oidc/node/public/images/favicon.ico b/auth-oidc/node/public/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8f688bed87fc160ec873ed29f827dab6ef62b71c GIT binary patch literal 2247 zcmV;&2srnNP)&H!#vjgP;#EPD z`C&5ANgRF%Iywquq7e;_8m*5J#TEsomPcP;q4ojN_TKi~d-h&yew1R7i*0Y)flOG* zP4+ow?Y+Nm@3p_Z_P)SbJB5kR4c&FWTh>)~`*?Pv#;f7wXbrQ)Lk!$Io&=onYB<@Z z2pTMky1zEnb8QhJ>8QhUL%XvL$yYziXP_4>7AJeB*m8`k&)QVB4iL zUh})+Z%{DjmW5yJzxIcGQ+0Jows@PF-W#gjNdDxzoN)dEn@m2t*}Y)<aH;zT$qa{G|KR8&aUG`3KyP@HZ!uN-&c;UYePS>vfi_Xrv ziM@D2X{>{?FXi_6UshE_z4UU~6!PMJRJ-aT5}FC(xCU>F_w;;OQ(o>5)#nXIci#fd zKXOF>X>;y*ohK4_zHC20O%&_hUweJ=Uq_R%Uf$4B z0zWw!jP_vr*UeQG6=vY`m96^<1A>Q@tM1joKtTND1}&+_wiL|$Dy@v}Us3Q$)84$L zHr~BL7@az|;E6sXO9#G}ymoLpiMv6>$@lxbJ4OW_0ElF3>GXPVa5o$Aj!c{wJV9Vn zSm{oh{DAoQ(vSBodwXKpZG*w5-v?sEO?OUwF_oZ&rYC-@kzoH$x=}PG-DeQUj`|7j zzCAY6OhH-hkyV7Q^#l{ihE6Vp4|Sh0cF#{}g2cB_bvZBy~wGEkSkek?L~#PC{d%7GB>} zsIqi9gnz;!5BRZ|JO*L+x7`gKM#aL%(wo*+$ksDQKVz+E}DnZTdD`OqY#Y_TS` zNu*lpmz8x7nP%74W_u>If;6onl0z!s{PyG7Ga83SASMpb>=b2%wTcAS6Nx?4o_{A| zWJU^p>gGd{u$2Ng~XW`=T*_@(I3o4v(~f~O$Z*H zHX(09DCN2ZY{?O;w`Kw!F;S7Ogd?eNcipG!+eQi=0JhGX`6@F#C6+Ev_Q->$mQfMP zU(<78Ku69KL;7f>A>lv=#ZtFnyDy{AFRsyEdxzh6Ykm)6G0t@Hhrexp&RVJxrn?vK zsJ}2RfxO3iFBL20VpMOJ&D)P(m;3CKW<#AU!H$f2ez%cZwzj13y)=5HR#j191nL>K zlxJA4IEg^`kG08CpPBH3+ z1=WWlB0|Rr5Ifhir70%ez!k_OiEqA7P$gCm@c`aUTO1tjn}_7oaPzu z5f;`Uc9PjMO8Wtcm3e?BB9%7Z=*h|HhSz0rxLyM|tuw(wQTP@Bf794kBdVVj)mnor zPMVzbn-Qgu=;*~asnfm=*x(9Iafe5`VflguRsn!zy;m%`!ZhcUHJ^UT2WstD-DG5# zPt6f=udr5qw6|hvXN-vZWz@$W5$d#-mI87LfHB6VNu- z??O}seub-s(Dc(1M$wIJ*NV=%+a!8F$UdiN%gL#P###WqV64U}nS?XQqR+Qoq!3)k zJ|_kG!J0wmhwliirQHxb37{Mn&ta7mp9$b+K6iL20^uzJXR7s*_tgKiaQhFO2ZXQ^ zk3jGT0AE4yI2dnYJ$e6Y;ODOGnJb#_B}e~;6nd3S+|zVd(F?;nk=-pT0Xz!AQ~=u{ zsDi?7FcQ!JlqC%SN<(W4=ws8Vaja~i6^_5x8i3|O<$+s=L3POzX$mWiC=?cxrYm(; zwn8%gks$f|mX(vXjqFgrN=jg{3cyVul))L~tpHL0>3+~O@Jxfsnf^ooljv4jeJBpE zZ>`?*I@X^0+Hq)2|E1)>K^ZL01rRx=MTBD`snx(Jr<;jmql{y|$KP{*8H3xfP>%B8 zpcTRm?e}L7>%W4b35XCL?GGFrKH0xCU7fN_BflPb!BSq{3LA?NEW literal 0 HcmV?d00001 diff --git a/auth-oidc/node/views/authfailed.ejs b/auth-oidc/node/views/authfailed.ejs new file mode 100644 index 000000000..aba3189a1 --- /dev/null +++ b/auth-oidc/node/views/authfailed.ejs @@ -0,0 +1,29 @@ + + + + + + + + <%= pageTitle %> + + +

Authentication failed!

+
OIDC configuration properties:
+
+ + ClientId=<%= clientId %> +
+ AuthorizationEndpoint=<%= providerAuthorizationEndpoint %> +
+ TokenEndpoint=<%= providerTokenEndpoint %> +
+ UserInfoEndpoint=<%= providerUserInfoEndpoint %> +
+
+
+ Login + + + + \ No newline at end of file diff --git a/auth-oidc/node/views/index.ejs b/auth-oidc/node/views/index.ejs new file mode 100644 index 000000000..e15f8dd0f --- /dev/null +++ b/auth-oidc/node/views/index.ejs @@ -0,0 +1,37 @@ + + + + + + + + <%= pageTitle %> + + + <% if(typeof user == 'object' && user?.token){ %> +

Your are authenticated!

+
OIDC configuration properties:
+
+ + ClientId=<%= clientId %> +
+ AuthorizationEndpoint=<%= providerAuthorizationEndpoint %> +
+ TokenEndpoint=<%= providerTokenEndpoint %> +
+ UserInfoEndpoint=<%= providerUserInfoEndpoint %> +
+
+
+
Access Token:
<%= user.token %>
+
+
User Profile:
<%= user.profile %>
+
+ Logout + <% } else{ %> + Login + <% } %> + + + + \ No newline at end of file From 1d71382d4a666a440606a4ae60c16da57f26ff2e Mon Sep 17 00:00:00 2001 From: Enrico Regge Date: Wed, 26 Nov 2025 11:16:47 +0100 Subject: [PATCH 6/9] Update index.mjs --- auth-oidc/node/index.mjs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/auth-oidc/node/index.mjs b/auth-oidc/node/index.mjs index d7bf8bbf5..e6d4304e7 100644 --- a/auth-oidc/node/index.mjs +++ b/auth-oidc/node/index.mjs @@ -1,5 +1,5 @@ import express from "express"; -import crypto from 'crypto'; +import crypto from "crypto"; import cookieParser from "cookie-parser"; import path from "path"; import { fileURLToPath } from "url"; @@ -30,7 +30,10 @@ if (missingEnvVars.length > 0) { const SESSION_COOKIE = process.env.COOKIE_NAME || "session_token"; const ENCRYPTION_KEY = Buffer.from(process.env.COOKIE_ENCRYPTION_KEY, "base64"); -const ENCRYPTION_IV = crypto.randomBytes(16); +let ENCRYPTION_IV = crypto.randomBytes(16); +if (process.env.COOKIE_ENCRYPTION_IV) { + ENCRYPTION_IV = Buffer.from(process.env.COOKIE_ENCRYPTION_IV, "base64"); +} const ENCRYPTION_ALGORITHM = "aes-256-cbc"; // check whether the KEY has got 32 bytes (256-bit) @@ -112,11 +115,13 @@ async function checkAuth(req, res, next) { Authorization: `Bearer ${sessionToken}`, }, }; - + // exchange authorization code for access token & id_token const response = await fetch(process.env.OIDC_PROVIDER_USERINFO_ENDPOINT, opts); - console.log(`fetched user data from '${process.env.OIDC_PROVIDER_USERINFO_ENDPOINT}' response.ok: '${response.ok}', response.status: '${response.status}'`); + console.log( + `fetched user data from '${process.env.OIDC_PROVIDER_USERINFO_ENDPOINT}' response.ok: '${response.ok}', response.status: '${response.status}'` + ); if (!response.ok) { const errorResponse = await response.text(); console.log(`errorResponse: '${errorResponse}'`); @@ -178,9 +183,7 @@ router.get("/auth/callback", async (req, res) => { // encrypt the access token const sessionCookieValue = encrypt(accessTokenData.access_token, ENCRYPTION_KEY, ENCRYPTION_IV); const maxAge = accessTokenData.expires_in ? 1000 * accessTokenData.expires_in : 600_000; // defaults to 10min - console.log( - `Setting session cookie '${SESSION_COOKIE}' (max age: '${maxAge}ms')` - ); + console.log(`Setting session cookie '${SESSION_COOKIE}' (max age: '${maxAge}ms')`); res.cookie(SESSION_COOKIE, sessionCookieValue, { maxAge, From 20556d15e3cb4481d91ed8888af9fb2a12c76baa Mon Sep 17 00:00:00 2001 From: Enrico Regge Date: Wed, 26 Nov 2025 11:34:27 +0100 Subject: [PATCH 7/9] Adjusted the README --- auth-oidc/.gitignore | 4 +- auth-oidc/README.md | 104 ++++++++++++++++++++++++----------- auth-oidc/ce-oidc.simple.png | Bin 0 -> 63912 bytes 3 files changed, 75 insertions(+), 33 deletions(-) create mode 100644 auth-oidc/ce-oidc.simple.png diff --git a/auth-oidc/.gitignore b/auth-oidc/.gitignore index 2eea525d8..67d5f0633 100644 --- a/auth-oidc/.gitignore +++ b/auth-oidc/.gitignore @@ -1 +1,3 @@ -.env \ No newline at end of file +.env +oidc-env.properties +oidc.properties \ No newline at end of file diff --git a/auth-oidc/README.md b/auth-oidc/README.md index 330885c45..23ee3aad8 100644 --- a/auth-oidc/README.md +++ b/auth-oidc/README.md @@ -1,53 +1,93 @@ -# OAuth 2.0 / OIDC +# OIDC sample -A pretty simple golang application that, in its most basic form, will -return "Hello World" back to the caller. +This example demonstrates how to OIDC authentication can be added to protect an IBM Cloud Code Engine app. -Check the source code for all of the things you can make it do either via -environment variables or query parameters. This is good for testing the -system to see how it reacts - for example, when the app crashes. +![](ce-oidc.simple.png) -Note: we added some extra logic to this so I can also be used as a batch job -but you can ignore that if all you care about is the App side of things. +## Setting up an OIDC SSO configuration +In order to be able to authenticate using OIDC SSO, you'll need to choose and configure a suitable OIDC provider. For this sample we demonstrate how this can be achieved by either using GitHub, or an IBM-internal provider. While many other OIDC providers will also work out-of-the-box, some may require few adjustments in the implementation of the auth app that we provide in this sample. +### Github.com OIDC SSO + +GitHub.com provides a publicly available OIDC provider, that can be used to point to Code Engine applications, which you deployed in your IBM Cloud account. Use the following steps to configure an SSO app: + +* Create Github OIDC app through https://github.com/settings/developers + ``` + name: oidc-sample + homepage: https://oidc-sample...codeengine.appdomain.cloud + callback URL: https://oidc-sample...codeengine.appdomain.cloud/auth/callback + ``` +* Store the client id and the secret in local file called `oidc.properties` + ``` + echo "OIDC_CLIENT_ID=" > oidc.properties + echo "OIDC_CLIENT_SECRET=" >> oidc.properties + ``` +* Generate a random cookie secret that is used to encrypt the auth cookie value and add it to the `oidc.properties` file + ``` + echo "COOKIE_ENCRYPTION_KEY=$(openssl rand -base64 32)" >> oidc.properties + ``` +* From your OIDC provider obtain the following values and add them to the `oidc.properties` file + ``` + echo "OIDC_PROVIDER_AUTHORIZATION_ENDPOINT=https://github.com/login/oauth/authorize" >> oidc.properties + echo "OIDC_PROVIDER_TOKEN_ENDPOINT=https://github.com/login/oauth/access_token" >> oidc.properties + echo "OIDC_PROVIDER_USERINFO_ENDPOINT=https://api.github.com/user" >> oidc.properties + ``` + +### IBMers-only: w3Id OIDC SSO + +To protect IBM's workforce, the SSO Provisioner provides the ability to configure an w3Id SSO. Note: This SSO provider can only be used by IBMers + +* Create w3Id OIDC configuration through https://w3.ibm.com/security/sso-provisioner + ``` + name: oidc-sample + homepage: https://oidc-sample...codeengine.appdomain.cloud + callback URL: https://oidc-sample...codeengine.appdomain.cloud/auth/callback + ``` +* Store the client id and the secret in local file called `oidc.properties` + ``` + echo "OIDC_CLIENT_ID=" > oidc.properties + echo "OIDC_CLIENT_SECRET=" >> oidc.properties + ``` +* Generate a random cookie secret that is used to encrypt the auth cookie value and add it to the `oidc.properties` file + ``` + echo "COOKIE_ENCRYPTION_KEY=$(openssl rand -base64 32)" >> oidc.properties + ``` +* From your OIDC provider obtain the following values and add them to the `oidc.properties` file + ``` + echo "OIDC_PROVIDER_AUTHORIZATION_ENDPOINT=" >> oidc.properties + echo "OIDC_PROVIDER_TOKEN_ENDPOINT=" >> oidc.properties + echo "OIDC_PROVIDER_USERINFO_ENDPOINT=" >> oidc.properties + ``` + +## Setup and Configuration + +Make sure the file `oidc.properties` contains the following properties are defined and contain meaningful values ``` -OIDC_CLIENT_ID= > .env -OIDC_CLIENT_SECRET= >> .env -OIDC_PROVIDER_AUTHORIZATION_ENDPOINT= >> .env -OIDC_PROVIDER_TOKEN_ENDPOINT= >> .env -OIDC_PROVIDER_USERINFO_ENDPOINT= >> .env +OIDC_CLIENT_ID +OIDC_CLIENT_SECRET +COOKIE_ENCRYPTION_KEY +OIDC_PROVIDER_AUTHORIZATION_ENDPOINT +OIDC_PROVIDER_TOKEN_ENDPOINT +OIDC_PROVIDER_USERINFO_ENDPOINT ``` * Create the secret ``` -ibmcloud ce secret create --name oidc-credentials --from-env-file .env +ibmcloud ce secret create --name oidc-credentials --from-env-file oidc.properties ``` * Create the application ``` ENCRYPTION_KEY=$(openssl rand -base64 32) -LANGUAGE=go -ibmcloud ce app create --name oidc-sample-$LANGUAGE \ +LANGUAGE=node +CE_SUBDOMAIN=$(ibmcloud ce proj current -o json |jq -r '.kube_config_context') +REGION=$(ibmcloud ce proj current -o json |jq -r '.region_id') +ibmcloud ce app create --name oidc-sample \ --src "." \ --build-context-dir "$LANGUAGE" \ --cpu 0.125 \ --memory 0.25G \ --env-from-secret oidc-credentials \ - --env COOKIE_ENCRYPTION_KEY=$ENCRYPTION_KEY \ - --env OIDC_REDIRECT_URL=https://oidc-sample-$LANGUAGE.1ryejitws058.eu-es.codeengine.appdomain.cloud/auth/callback - -OIDC_REDIRECT_URL=$(ibmcloud ce app get -n oidc-sample-$LANGUAGE --output url) -ibmcloud ce app update --name oidc-sample-$LANGUAGE --env OIDC_REDIRECT_URL=$OIDC_REDIRECT_URL/auth/callback -cd .. + --env OIDC_REDIRECT_URL=https://oidc-sample.$CE_SUBDOMAIN.$REGION.codeengine.appdomain.cloud/auth/callback ``` - -- - - - -As noted in [the main README](../README.md), this sample has two pieces: - -- a `build` script which will build the container image(s) used -- a `run` script which deploys resources that use those images - -The main purpose of this example is the `run` script, but the `build` -script is included for complete educational (and reuse) purposes. diff --git a/auth-oidc/ce-oidc.simple.png b/auth-oidc/ce-oidc.simple.png new file mode 100644 index 0000000000000000000000000000000000000000..9ab5557545d9e2eff6ef93fa3f094831b3ce6b67 GIT binary patch literal 63912 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 MS_WqeH80=!H+-}6t^fc4 literal 0 HcmV?d00001 From 18b6037036357f5c94b11beaf893e04a731f38b2 Mon Sep 17 00:00:00 2001 From: Enrico Regge Date: Wed, 26 Nov 2025 11:40:43 +0100 Subject: [PATCH 8/9] Update README.md --- auth-oidc/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth-oidc/README.md b/auth-oidc/README.md index 23ee3aad8..709fc25fc 100644 --- a/auth-oidc/README.md +++ b/auth-oidc/README.md @@ -36,7 +36,7 @@ GitHub.com provides a publicly available OIDC provider, that can be used to poin ### IBMers-only: w3Id OIDC SSO -To protect IBM's workforce, the SSO Provisioner provides the ability to configure an w3Id SSO. Note: This SSO provider can only be used by IBMers +To protect IBM-owned, internal applications, the w3Id SSO Provisioner provides the ability to configure an w3Id SSO. Note: This SSO provider can only be used by IBMers * Create w3Id OIDC configuration through https://w3.ibm.com/security/sso-provisioner ``` From a9c8bedf86f9d216cdddc3bff1626d1f70bdcb4f Mon Sep 17 00:00:00 2001 From: Enrico Regge Date: Wed, 26 Nov 2025 11:44:12 +0100 Subject: [PATCH 9/9] Remove the old oidc folder --- oidc/.gitignore | 1 - oidc/README.md | 53 -- oidc/go/.ceignore | 1 - oidc/go/Dockerfile | 11 - oidc/go/app.go | 388 ----------- oidc/go/build | 19 - oidc/go/go.mod | 8 - oidc/go/go.sum | 6 - oidc/go/static/auth-failed.html | 29 - oidc/go/static/auth-logout.html | 16 - oidc/go/static/css/style.css | 13 - oidc/go/static/home.html | 39 -- oidc/go/static/images/favicon.ico | Bin 2247 -> 0 bytes oidc/node/Dockerfile | 14 - oidc/node/build | 19 - oidc/node/index.mjs | 201 ------ oidc/node/package-lock.json | 975 ---------------------------- oidc/node/package.json | 17 - oidc/node/public/css/style.css | 13 - oidc/node/public/images/favicon.ico | Bin 2247 -> 0 bytes oidc/node/views/authfailed.ejs | 29 - oidc/node/views/index.ejs | 37 -- 22 files changed, 1889 deletions(-) delete mode 100644 oidc/.gitignore delete mode 100644 oidc/README.md delete mode 100644 oidc/go/.ceignore delete mode 100644 oidc/go/Dockerfile delete mode 100644 oidc/go/app.go delete mode 100755 oidc/go/build delete mode 100644 oidc/go/go.mod delete mode 100644 oidc/go/go.sum delete mode 100644 oidc/go/static/auth-failed.html delete mode 100644 oidc/go/static/auth-logout.html delete mode 100644 oidc/go/static/css/style.css delete mode 100644 oidc/go/static/home.html delete mode 100644 oidc/go/static/images/favicon.ico delete mode 100644 oidc/node/Dockerfile delete mode 100755 oidc/node/build delete mode 100644 oidc/node/index.mjs delete mode 100644 oidc/node/package-lock.json delete mode 100644 oidc/node/package.json delete mode 100644 oidc/node/public/css/style.css delete mode 100644 oidc/node/public/images/favicon.ico delete mode 100644 oidc/node/views/authfailed.ejs delete mode 100644 oidc/node/views/index.ejs diff --git a/oidc/.gitignore b/oidc/.gitignore deleted file mode 100644 index 2eea525d8..000000000 --- a/oidc/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.env \ No newline at end of file diff --git a/oidc/README.md b/oidc/README.md deleted file mode 100644 index 2697f6715..000000000 --- a/oidc/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# OAuth2 / OIDC - -A pretty simple golang application that, in its most basic form, will -return "Hello World" back to the caller. - -Check the source code for all of the things you can make it do either via -environment variables or query parameters. This is good for testing the -system to see how it reacts - for example, when the app crashes. - -Note: we added some extra logic to this so I can also be used as a batch job -but you can ignore that if all you care about is the App side of things. - - -``` -OIDC_CLIENT_ID= > .env -OIDC_CLIENT_SECRET= >> .env -OIDC_PROVIDER_AUTHORIZATION_ENDPOINT= >> .env -OIDC_PROVIDER_TOKEN_ENDPOINT= >> .env -OIDC_PROVIDER_USERINFO_ENDPOINT= >> .env -``` - -* Create the secret -``` -ibmcloud ce secret create --name oidc-credentials --from-env-file .env -``` - -* Create the application -``` -ENCRYPTION_KEY=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64 | tr -d -- '\n' | tr -- '+/' '-_' ; echo) -LANGUAGE=go -ibmcloud ce app create --name oidc-sample-$LANGUAGE \ - --src "." \ - --build-context-dir "$LANGUAGE" \ - --cpu 0.125 \ - --memory 0.25G \ - --env-from-secret oidc-credentials \ - --env COOKIE_SIGNING_PASSPHRASE=$ENCRYPTION_KEY \ - --env OIDC_REDIRECT_URL=https://oidc-sample-$LANGUAGE.1ryejitws058.eu-es.codeengine.appdomain.cloud/auth/callback - -OIDC_REDIRECT_URL=$(ibmcloud ce app get -n oidc-sample-$LANGUAGE --output url) -ibmcloud ce app update --name oidc-sample-$LANGUAGE --env OIDC_REDIRECT_URL=$OIDC_REDIRECT_URL/auth/callback -cd .. -``` - -- - - - -As noted in [the main README](../README.md), this sample has two pieces: - -- a `build` script which will build the container image(s) used -- a `run` script which deploys resources that use those images - -The main purpose of this example is the `run` script, but the `build` -script is included for complete educational (and reuse) purposes. diff --git a/oidc/go/.ceignore b/oidc/go/.ceignore deleted file mode 100644 index 2eea525d8..000000000 --- a/oidc/go/.ceignore +++ /dev/null @@ -1 +0,0 @@ -.env \ No newline at end of file diff --git a/oidc/go/Dockerfile b/oidc/go/Dockerfile deleted file mode 100644 index 71a9de7eb..000000000 --- a/oidc/go/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM quay.io/projectquay/golang:1.22 AS build-env -WORKDIR /go/src/app -COPY . . - -RUN CGO_ENABLED=0 GOOS=linux go build -o /go/bin/app app.go - -# Copy the exe into a smaller base image -FROM gcr.io/distroless/static-debian12 -COPY --from=build-env /go/bin/app / -COPY static /static -ENTRYPOINT ["/app"] diff --git a/oidc/go/app.go b/oidc/go/app.go deleted file mode 100644 index e13498c1e..000000000 --- a/oidc/go/app.go +++ /dev/null @@ -1,388 +0,0 @@ -package main - -import ( - "encoding/json" - "errors" - "fmt" - "html/template" - "log" - "net/http" - "os" - "os/signal" - "syscall" - "time" - - "golang.org/x/net/context" - "golang.org/x/oauth2" -) - -var conf oauth2.Config -var oidcConfig OIDCConfig - -const STATE = "state" -const SESSION_TOKEN = "session_token" - -// Home struct, used for home.html template -type Home struct { - Title string - User User - ClientId string - ProviderAuthorizationEndpoint string - ProviderTokenEndpoint string - ProviderUserInfoEndpoint string -} - -// User struct, holds all the user info shown in home.html -type User struct { - Token string - Profile string -} - -// OIDC configuration struct -type OIDCConfig struct { - ClientId string - ClientSecret string - ProviderAuthorizationEndpoint string - ProviderTokenEndpoint string - ProviderUserInfoEndpoint string - RedirectUrl string -} - -// Loads the OIDC configuration from environment variables -func loadOIDCConfig() (oidcConfiguration OIDCConfig, err error) { - oidcConfiguration.ClientId = os.Getenv("OIDC_CLIENT_ID") - if oidcConfiguration.ClientId == "" { - err = fmt.Errorf("missing %s environment variable", "OIDC_CLIENT_ID") - return - } - - oidcConfiguration.ClientSecret = os.Getenv("OIDC_CLIENT_SECRET") - if oidcConfiguration.ClientSecret == "" { - err = fmt.Errorf("missing %s environment variable", "OIDC_CLIENT_SECRET") - return - } - oidcConfiguration.ProviderAuthorizationEndpoint = os.Getenv("OIDC_PROVIDER_AUTHORIZATION_ENDPOINT") - if oidcConfiguration.ProviderAuthorizationEndpoint == "" { - err = fmt.Errorf("missing %s environment variable", "OIDC_PROVIDER_AUTHORIZATION_ENDPOINT") - return - } - oidcConfiguration.ProviderTokenEndpoint = os.Getenv("OIDC_PROVIDER_TOKEN_ENDPOINT") - if oidcConfiguration.ProviderTokenEndpoint == "" { - err = fmt.Errorf("missing %s environment variable", "OIDC_PROVIDER_TOKEN_ENDPOINT") - return - } - oidcConfiguration.ProviderUserInfoEndpoint = os.Getenv("OIDC_PROVIDER_USERINFO_ENDPOINT") - if oidcConfiguration.ProviderUserInfoEndpoint == "" { - err = fmt.Errorf("missing %s environment variable", "OIDC_PROVIDER_USERINFO_ENDPOINT") - return - } - oidcConfiguration.RedirectUrl = os.Getenv("OIDC_REDIRECT_URL") - if oidcConfiguration.RedirectUrl == "" { - err = fmt.Errorf("missing %s environment variable", "OIDC_REDIRECT_URL") - return - } - return oidcConfiguration, nil -} - -// Requests an OAuthToken using a "code" type -func GetOauthToken(r *http.Request) (*oauth2.Token, error) { - - log.Println("Getting auth token.") - - ctx := context.Background() - - if ctx == nil { - return nil, errors.New("could not get context") - } - - if r.URL.Query().Get(STATE) != STATE { - return nil, errors.New("state value did not match") - } - - // Exchange code for OAuth token - oauth2Token, oauth2TokenError := conf.Exchange(ctx, r.URL.Query().Get("code")) - if oauth2TokenError != nil { - return nil, errors.New("Failed to exchange token:" + oauth2TokenError.Error()) - } - - return oauth2Token, nil -} - -// Requests a user profile, using a bearer token -func GetUserProfile(r *http.Request, token oauth2.Token) (interface{}, error) { - - log.Println("Getting user profile ...") - - ctx := context.Background() - - if ctx == nil { - return nil, errors.New("could not get context") - } - - // Getting now the userInfo - client := conf.Client(ctx, &token) - - // Get request using /userinfo url - userinfoResponse, userinfoError := client.Get(oidcConfig.ProviderUserInfoEndpoint) - if userinfoError != nil { - return nil, errors.New("Failed to obtain userinfo:" + userinfoError.Error()) - } - - defer userinfoResponse.Body.Close() - - log.Println("Getting user profile: " + userinfoResponse.Status) - - if userinfoResponse.StatusCode != http.StatusOK { - return nil, errors.New("HTTP status is not 200. Was " + userinfoResponse.Status + "; response: " + toJSONString(userinfoResponse.Body)) - } - - // Decoding profile info and putting it in a map, to make it more readable - var profile map[string]interface{} - if userinfoError = json.NewDecoder(userinfoResponse.Body).Decode(&profile); userinfoError != nil { - return nil, userinfoError - } - - return profile, nil - -} - -// Home handler for /home -func home(w http.ResponseWriter, r *http.Request) { - - log.Printf("Executing /home for '%s'", r.RequestURI) - - // Parssing home.html template - tmpl, _ := template.ParseFiles("./static/home.html") - data := &Home{} - - // Adding title to page - data.Title = "OIDC sample - IBM Cloud Code Engine" - - // Getting cookie named SESSION_TOKEN - cookie, err := r.Cookie(SESSION_TOKEN) - - if err != nil { - - // If no cookie found, that's ok, that means no user is logged in - log.Println("No session cookie found:" + err.Error()) - - // Redirecting to /, in order to show the logged in user values - http.Redirect(w, r, "/auth/login", http.StatusSeeOther) - } else { - - log.Println("Session cookie found.") - - // A cookie was found, this means a user is logged in - // Let's get the auth token value - - authToken := oauth2.Token{ - AccessToken: cookie.Value, - } - - // Getting the user profile for the given auth token - profile, profileError := GetUserProfile(r, authToken) - - if profileError != nil { - log.Print("Error getting profile. Error: " + profileError.Error()) - - // Redirecting to /auth/failed, in order to avoid an endless redirect loop - http.Redirect(w, r, "/auth/failed", http.StatusSeeOther) - return - } - - // Exposing OIDC configuration values - data.ClientId = oidcConfig.ClientId - data.ProviderAuthorizationEndpoint = oidcConfig.ProviderAuthorizationEndpoint - data.ProviderTokenEndpoint = oidcConfig.ProviderTokenEndpoint - data.ProviderUserInfoEndpoint = oidcConfig.ProviderUserInfoEndpoint - - // Setting values in page template, this is what we are going to show for the logged in user - data.User.Token = fmt.Sprintln(authToken.AccessToken) - data.User.Profile = fmt.Sprintln(profile) - - log.Println("User already logged in:" + fmt.Sprintln(profile)) - - } - - tmpl.ExecuteTemplate(w, "home", data) - -} - -// Home handler for /auth/failed -func authFailed(w http.ResponseWriter, r *http.Request) { - - log.Println("Executing /auth/failed") - - // Parssing auth-failed.html template - tmpl, _ := template.ParseFiles("./static/auth-failed.html") - data := &Home{} - - // Adding title to page - data.Title = "Authentication Failed" - - // Exposing OIDC configuration values - data.ClientId = oidcConfig.ClientId - data.ProviderAuthorizationEndpoint = oidcConfig.ProviderAuthorizationEndpoint - data.ProviderTokenEndpoint = oidcConfig.ProviderTokenEndpoint - data.ProviderUserInfoEndpoint = oidcConfig.ProviderUserInfoEndpoint - - w.WriteHeader(http.StatusUnauthorized) - tmpl.ExecuteTemplate(w, "authFailed", data) -} - -// Login handler for /auth/login -func authLogin(w http.ResponseWriter, r *http.Request) { - - log.Println("Executing /auth/login") - - // Code request to Auth URL - http.Redirect(w, r, conf.AuthCodeURL(STATE), http.StatusFound) -} - -// Handler for /auth/callback -func authCallback(w http.ResponseWriter, r *http.Request) { - - log.Println("Executing /auth/callback") - - // Getting auth token from request - authToken, error := GetOauthToken(r) - - if error != nil { - - log.Println("Error getting auth token. Error: " + error.Error()) - - // Redirecting to /auth/failed, in order to avoid an endless redirect loop - http.Redirect(w, r, "/auth/failed", http.StatusSeeOther) - return - } - - log.Println("Setting session cookie.") - - // Setting cookie with the value of this auth token - - http.SetCookie(w, &http.Cookie{ - Name: "session_token", - Value: authToken.AccessToken, - Path: "/", - Expires: time.Now().Add(1000 * time.Second), - HttpOnly: true, - Secure: true, - }) - - // Redirecting to /, in order to show the logged in user values - http.Redirect(w, r, "/", http.StatusSeeOther) -} - -// Logout handler for /auth/logout -func authLogout(w http.ResponseWriter, r *http.Request) { - - log.Println("Executing /auth/logout") - - // Parssing auth-none.html template - tmpl, _ := template.ParseFiles("./static/auth-logout.html") - data := &Home{} - - // Adding title to page - data.Title = "Logged out" - - // Getting session cookie - cookie, err := r.Cookie(SESSION_TOKEN) - - if err != nil { - - log.Println("No session cookie found:" + err.Error()) - - } else { - - log.Println("Session cookie found, invalidating it.") - - // If cookie was found, let's invalidate it - cookie.Value = "" - cookie.Expires = time.Unix(0, 0) - cookie.MaxAge = -1 - cookie.HttpOnly = true - } - - // Setting the invalidated cookie - http.SetCookie(w, cookie) - w.WriteHeader(http.StatusFound) - tmpl.ExecuteTemplate(w, "authLogout", data) -} - -func main() { - ctx := context.Background() - signals := make(chan os.Signal, 1) - signal.Notify(signals, os.Interrupt, syscall.SIGTERM) - log.Println("Starting app ...") - - // Load OIDC relevant config parameters - var err error - oidcConfig, err = loadOIDCConfig() - if err != nil { - log.Println("Aborting! Could not load OIDC config. Error: " + err.Error()) - os.Exit(1) - - } - - log.Println("Redirect URL: '" + oidcConfig.RedirectUrl + "'") - - // Building global conf object, using OAuth2/OIDC configuration - conf = oauth2.Config{ - ClientID: oidcConfig.ClientId, - ClientSecret: oidcConfig.ClientSecret, - RedirectURL: oidcConfig.RedirectUrl, - Scopes: []string{"openid", "profile"}, - Endpoint: oauth2.Endpoint{ - AuthURL: oidcConfig.ProviderAuthorizationEndpoint, - TokenURL: oidcConfig.ProviderTokenEndpoint, - }, - } - - // Serving static files - fs := http.FileServer(http.Dir("static")) - - // Creating handlers: /static /home /login /auth/callback /logout - http.Handle("/static/", http.StripPrefix("/static/", fs)) - http.HandleFunc("/auth/login", authLogin) - http.HandleFunc("/auth/callback", authCallback) - http.HandleFunc("/auth/logout", authLogout) - http.HandleFunc("/auth/failed", authFailed) - http.HandleFunc("/", home) - - // Using port 8080 - port := ":8080" - srv := &http.Server{Addr: port} - - // Launch the HTTP server - go func() { - - log.Printf("An instance of application '%s' has been started on port %s :)", os.Getenv("CE_APP"), port) - - if err := srv.ListenAndServe(); err != http.ErrServerClosed { - log.Fatalf("Failed to start server: %v", err) - } - }() - - // Add a SIGTERM listener to properly shutdown the app - <-signals - log.Println("Shutting down server") - if err := srv.Shutdown(ctx); err != nil { - log.Fatalf("Failed to shutdown server: %v", err) - } - log.Println("Shutdown done") - -} - -// Helper function that converts any object into a JSON string representation -func toJSONString(obj interface{}) string { - if obj == nil { - return "" - } - - bytes, err := json.Marshal(&obj) - if err != nil { - return "marshal error: " + err.Error() - } - - return string(bytes) -} diff --git a/oidc/go/build b/oidc/go/build deleted file mode 100755 index c8b4bceb8..000000000 --- a/oidc/go/build +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -# Env Vars: -# REGISTRY: name of the image registry/namespace to store the images -# NOCACHE: set this to "--no-cache" to turn off the Docker build cache -# -# NOTE: to run this you MUST set the REGISTRY environment variable to -# your own image registry/namespace otherwise the `docker push` commands -# will fail due to an auth failure. Which means, you also need to be logged -# into that registry before you run it. - -set -ex -export REGISTRY=${REGISTRY:-icr.io/codeengine} - -# Build the image -docker build ${NOCACHE} -t ${REGISTRY}/oidc/go . --platform linux/amd64 - -# And push it -docker push ${REGISTRY}/oidc/go diff --git a/oidc/go/go.mod b/oidc/go/go.mod deleted file mode 100644 index b394bc96b..000000000 --- a/oidc/go/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -module github.ibm.com/CodeEngine/oidc - -go 1.22.5 - -require ( - golang.org/x/net v0.35.0 - golang.org/x/oauth2 v0.26.0 -) diff --git a/oidc/go/go.sum b/oidc/go/go.sum deleted file mode 100644 index 425f5a375..000000000 --- a/oidc/go/go.sum +++ /dev/null @@ -1,6 +0,0 @@ -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= -golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= -golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= diff --git a/oidc/go/static/auth-failed.html b/oidc/go/static/auth-failed.html deleted file mode 100644 index e7454c439..000000000 --- a/oidc/go/static/auth-failed.html +++ /dev/null @@ -1,29 +0,0 @@ -{{ define "authFailed" }} - - - - - - - - {{.Title}} - - -

Authentication failed!

-
OIDC configuration properties:
-
- - ClientId={{.ClientId}} -
- AuthorizationEndpoint={{.ProviderAuthorizationEndpoint}} -
- TokenEndpoint={{.ProviderTokenEndpoint}} -
- UserInfoEndpoint={{.ProviderUserInfoEndpoint}} -
-
-
- Login - - -{{ end }} \ No newline at end of file diff --git a/oidc/go/static/auth-logout.html b/oidc/go/static/auth-logout.html deleted file mode 100644 index ec316f93d..000000000 --- a/oidc/go/static/auth-logout.html +++ /dev/null @@ -1,16 +0,0 @@ -{{ define "authLogout" }} - - - - - - - - {{.Title}} - - -

Log out done!

- Login - - -{{ end }} diff --git a/oidc/go/static/css/style.css b/oidc/go/static/css/style.css deleted file mode 100644 index 71632b6b9..000000000 --- a/oidc/go/static/css/style.css +++ /dev/null @@ -1,13 +0,0 @@ -/* style.css */ -body,html { - background-color: #f4f4f4; - margin: 0 auto; - font-family: "IBM Plex Sans", sans-serif; - padding: 1rem; -} - -pre { - text-wrap: wrap; - word-break: break-word; - width: 95%; -} diff --git a/oidc/go/static/home.html b/oidc/go/static/home.html deleted file mode 100644 index 68f1e294b..000000000 --- a/oidc/go/static/home.html +++ /dev/null @@ -1,39 +0,0 @@ -{{ define "home" }} - - - - - - - - {{.Title}} - - - {{if .User.Token}} -

Your are authenticated!

-
OIDC configuration properties:
-
- - ClientId={{.ClientId}} -
- AuthorizationEndpoint={{.ProviderAuthorizationEndpoint}} -
- TokenEndpoint={{.ProviderTokenEndpoint}} -
- UserInfoEndpoint={{.ProviderUserInfoEndpoint}} -
-
-
-
Access Token:
{{.User.Token}}
-
-
User Profile:
{{.User.Profile}}
-
- Logout - {{else}} - Login - {{end}} - - - - -{{ end }} \ No newline at end of file diff --git a/oidc/go/static/images/favicon.ico b/oidc/go/static/images/favicon.ico deleted file mode 100644 index 8f688bed87fc160ec873ed29f827dab6ef62b71c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2247 zcmV;&2srnNP)&H!#vjgP;#EPD z`C&5ANgRF%Iywquq7e;_8m*5J#TEsomPcP;q4ojN_TKi~d-h&yew1R7i*0Y)flOG* zP4+ow?Y+Nm@3p_Z_P)SbJB5kR4c&FWTh>)~`*?Pv#;f7wXbrQ)Lk!$Io&=onYB<@Z z2pTMky1zEnb8QhJ>8QhUL%XvL$yYziXP_4>7AJeB*m8`k&)QVB4iL zUh})+Z%{DjmW5yJzxIcGQ+0Jows@PF-W#gjNdDxzoN)dEn@m2t*}Y)<aH;zT$qa{G|KR8&aUG`3KyP@HZ!uN-&c;UYePS>vfi_Xrv ziM@D2X{>{?FXi_6UshE_z4UU~6!PMJRJ-aT5}FC(xCU>F_w;;OQ(o>5)#nXIci#fd zKXOF>X>;y*ohK4_zHC20O%&_hUweJ=Uq_R%Uf$4B z0zWw!jP_vr*UeQG6=vY`m96^<1A>Q@tM1joKtTND1}&+_wiL|$Dy@v}Us3Q$)84$L zHr~BL7@az|;E6sXO9#G}ymoLpiMv6>$@lxbJ4OW_0ElF3>GXPVa5o$Aj!c{wJV9Vn zSm{oh{DAoQ(vSBodwXKpZG*w5-v?sEO?OUwF_oZ&rYC-@kzoH$x=}PG-DeQUj`|7j zzCAY6OhH-hkyV7Q^#l{ihE6Vp4|Sh0cF#{}g2cB_bvZBy~wGEkSkek?L~#PC{d%7GB>} zsIqi9gnz;!5BRZ|JO*L+x7`gKM#aL%(wo*+$ksDQKVz+E}DnZTdD`OqY#Y_TS` zNu*lpmz8x7nP%74W_u>If;6onl0z!s{PyG7Ga83SASMpb>=b2%wTcAS6Nx?4o_{A| zWJU^p>gGd{u$2Ng~XW`=T*_@(I3o4v(~f~O$Z*H zHX(09DCN2ZY{?O;w`Kw!F;S7Ogd?eNcipG!+eQi=0JhGX`6@F#C6+Ev_Q->$mQfMP zU(<78Ku69KL;7f>A>lv=#ZtFnyDy{AFRsyEdxzh6Ykm)6G0t@Hhrexp&RVJxrn?vK zsJ}2RfxO3iFBL20VpMOJ&D)P(m;3CKW<#AU!H$f2ez%cZwzj13y)=5HR#j191nL>K zlxJA4IEg^`kG08CpPBH3+ z1=WWlB0|Rr5Ifhir70%ez!k_OiEqA7P$gCm@c`aUTO1tjn}_7oaPzu z5f;`Uc9PjMO8Wtcm3e?BB9%7Z=*h|HhSz0rxLyM|tuw(wQTP@Bf794kBdVVj)mnor zPMVzbn-Qgu=;*~asnfm=*x(9Iafe5`VflguRsn!zy;m%`!ZhcUHJ^UT2WstD-DG5# zPt6f=udr5qw6|hvXN-vZWz@$W5$d#-mI87LfHB6VNu- z??O}seub-s(Dc(1M$wIJ*NV=%+a!8F$UdiN%gL#P###WqV64U}nS?XQqR+Qoq!3)k zJ|_kG!J0wmhwliirQHxb37{Mn&ta7mp9$b+K6iL20^uzJXR7s*_tgKiaQhFO2ZXQ^ zk3jGT0AE4yI2dnYJ$e6Y;ODOGnJb#_B}e~;6nd3S+|zVd(F?;nk=-pT0Xz!AQ~=u{ zsDi?7FcQ!JlqC%SN<(W4=ws8Vaja~i6^_5x8i3|O<$+s=L3POzX$mWiC=?cxrYm(; zwn8%gks$f|mX(vXjqFgrN=jg{3cyVul))L~tpHL0>3+~O@Jxfsnf^ooljv4jeJBpE zZ>`?*I@X^0+Hq)2|E1)>K^ZL01rRx=MTBD`snx(Jr<;jmql{y|$KP{*8H3xfP>%B8 zpcTRm?e}L7>%W4b35XCL?GGFrKH0xCU7fN_BflPb!BSq{3LA?NEW diff --git a/oidc/node/Dockerfile b/oidc/node/Dockerfile deleted file mode 100644 index 1f9de3d4e..000000000 --- a/oidc/node/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM registry.access.redhat.com/ubi9/nodejs-22:latest AS build-env -WORKDIR /app -COPY index.mjs . -COPY package.json . -RUN npm install - -# Use a small distroless image for as runtime image -FROM gcr.io/distroless/nodejs22-debian12 -COPY --from=build-env /app /app -WORKDIR /app -COPY public/ public/ -COPY views/ views/ -EXPOSE 8080 -CMD ["index.mjs"] \ No newline at end of file diff --git a/oidc/node/build b/oidc/node/build deleted file mode 100755 index dbaa4c486..000000000 --- a/oidc/node/build +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -# Env Vars: -# REGISTRY: name of the image registry/namespace to store the images -# NOCACHE: set this to "--no-cache" to turn off the Docker build cache -# -# NOTE: to run this you MUST set the REGISTRY environment variable to -# your own image registry/namespace otherwise the `docker push` commands -# will fail due to an auth failure. Which means, you also need to be logged -# into that registry before you run it. - -set -ex -export REGISTRY=${REGISTRY:-icr.io/codeengine} - -# Build the image -docker build ${NOCACHE} -t ${REGISTRY}/oidc/node . --platform linux/amd64 - -# And push it -docker push ${REGISTRY}/oidc/node diff --git a/oidc/node/index.mjs b/oidc/node/index.mjs deleted file mode 100644 index d59ec260c..000000000 --- a/oidc/node/index.mjs +++ /dev/null @@ -1,201 +0,0 @@ -// require expressjs -import express from "express"; -import fs from 'fs'; - -import cookieParser from "cookie-parser"; -import path from "path"; -import { fileURLToPath } from "url"; -const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file -const __dirname = path.dirname(__filename); // get the name of the directory - -const SESSION_COOKIE = "session_token"; - -const requiredEnvVars = [ - "OIDC_CLIENT_ID", - "OIDC_CLIENT_SECRET", - "OIDC_PROVIDER_AUTHORIZATION_ENDPOINT", - "OIDC_PROVIDER_TOKEN_ENDPOINT", - "OIDC_PROVIDER_USERINFO_ENDPOINT", - "OIDC_REDIRECT_URL", - "COOKIE_SIGNING_PASSPHRASE", -]; - -requiredEnvVars.forEach((envVarName) => { - if (!process.env[envVarName]) { - console.log(`Missing '${envVarName}' environment variable`); - process.exit(1); - } -}); - -function encrypt(plaintext, key, iv) { - const cipher = crypto.createCipheriv("aes-256-gcm", key, iv); - let ciphertext = cipher.update(plaintext, "utf8", "base64"); - ciphertext += cipher.final("base64"); - return ciphertext; -} - -function decrypt(ciphertext, key, iv) { - const decipher = crypto.createDecipheriv("aes-256-gcm", Buffer.from(key, "base64"), Buffer.from(iv, "base64")); - let plaintext = decipher.update(ciphertext, "base64", "utf8"); - plaintext += decipher.final("utf8"); - - return plaintext; -} - -// check whether the auth cookie is set -async function checkAuth(req, res, next) { - console.log(`performing auth check for '${req.url}'`); - - console.log("Cookies: ", req.cookies); - const encryptedSessionToken = req.cookies[SESSION_COOKIE]; - - if (!encryptedSessionToken) { - console.log(`session cookie '${SESSION_COOKIE}' not found`); - return res.redirect("/auth/login"); - } - - // decrypt session token - const sessionToken = decrypt(encryptedSessionToken, process.env.COOKIE_SIGNING_PASSPHRASE, IV); - - const opts = { - method: "GET", - headers: { - Authorization: `Bearer ${sessionToken}`, - }, - }; - console.log( - `Fetching user data from '${process.env.OIDC_PROVIDER_USERINFO_ENDPOINT}' with '${JSON.stringify(opts)}'...` - ); - // exchange authorization code for access token & id_token - const response = await fetch(process.env.OIDC_PROVIDER_USERINFO_ENDPOINT, opts); - - console.log(`response.ok: '${response.ok}', response.status: '${response.status}'`); - if (!response.ok) { - const errorResponse = await response.text(); - console.log(`errorResponse: '${errorResponse}'`); - return res.redirect("/auth/failed"); - } - - const user_data = await response.json(); - console.log(`user_data: '${JSON.stringify(user_data)}'`); - - // setting user into the request context - req.user = user_data; - - next(); -} - -const app = express(); -app.use(express.json()); -app.use(cookieParser()); - -// Define a view engine -app.set("view engine", "ejs"); -app.set("views", path.join(__dirname, "views")); - -// use router to bundle all routes to / -const router = express.Router(); - -app.use("/", router); - -router.get("/auth/callback", async (req, res) => { - console.log(`handling /auth/callback`); - - const { code } = req.query; - const data = { - code, - redirect_uri: process.env.OIDC_REDIRECT_URL, - grant_type: "authorization_code", - }; - - console.log(data); - - // exchange authorization code for access token & id_token - const response = await fetch(`${process.env.OIDC_PROVIDER_TOKEN_ENDPOINT}?${new URLSearchParams(data).toString()}`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - Authorization: - "Basic " + Buffer.from(process.env.OIDC_CLIENT_ID + ":" + process.env.OIDC_CLIENT_SECRET).toString("base64"), - }, - }); - - console.log(`response.ok: '${response.ok}', response.status: '${response.status}'`); - if (!response.ok) { - const errorResponse = await response.text(); - console.log(`errorResponse: '${errorResponse}'`); - return res.redirect("/auth/failed"); - } - - const access_token_data = await response.json(); - console.log(`access_token_data: '${JSON.stringify(access_token_data)}'`); - - // encrypt the access token - const sessionCookieValue = encrypt(access_token_data.access_token, process.env.COOKIE_SIGNING_PASSPHRASE, IV); - - console.log("Setting session cookie."); - res.cookie(SESSION_COOKIE, sessionCookieValue, { - maxAge: 1000 * access_token_data.expires_in, - httpOnly: true, - path: "/", - secure: true, - }); - - // redirect to the home route - return res.redirect("/"); -}); - -router.get("/auth/login", (req, res) => { - console.log(`handling /auth/login for '${req.url}'`); - console.log(`baseUrl: '${req.baseUrl}'`); - - // redirect to the configured OIDC provider - res.redirect( - `${process.env.OIDC_PROVIDER_AUTHORIZATION_ENDPOINT}?client_id=${ - process.env.OIDC_CLIENT_ID - }&redirect_uri=${encodeURIComponent( - process.env.OIDC_REDIRECT_URL - )}&response_type=code&scope=openid+profile&state=state` - ); -}); - -const viewParams = { - pageTitle: "OIDC sample - IBM Cloud Code Engine", - clientId: process.env.OIDC_CLIENT_ID, - providerAuthorizationEndpoint: process.env.OIDC_PROVIDER_AUTHORIZATION_ENDPOINT, - providerTokenEndpoint: process.env.OIDC_PROVIDER_TOKEN_ENDPOINT, - providerUserInfoEndpoint: process.env.OIDC_PROVIDER_USERINFO_ENDPOINT, -}; - -// route that renders an auth failed page -router.get("/auth/failed", (req, res) => { - console.log(`handling /auth/failed for '${req.url}'`); - res.status(401); - res.render("authfailed", viewParams); -}); - -// get on root route -router.get("/", checkAuth, (req, res) => { - console.log(`handling / for '${req.url}'`); - res.render("index", { - ...viewParams, - user: { token: req.cookies[SESSION_COOKIE], profile: JSON.stringify(req.user) }, - }); -}); - -// serve static files -app.use("/public", express.static("public")); - -// start server -const port = process.env.PORT || 8080; -const server = app.listen(port, () => { - console.log(`Server is up and running on port ${port}!`); -}); - -process.on("SIGTERM", () => { - console.info("SIGTERM signal received."); - server.close(() => { - console.log("Http server closed."); - }); -}); diff --git a/oidc/node/package-lock.json b/oidc/node/package-lock.json deleted file mode 100644 index f12a9eca9..000000000 --- a/oidc/node/package-lock.json +++ /dev/null @@ -1,975 +0,0 @@ -{ - "name": "code-engine-nodejs", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "code-engine-nodejs", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "cookie-parser": "^1.4.7", - "cookies": "^0.9.1", - "ejs": "^3.1.10", - "express": "^4.21.2" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-parser": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", - "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", - "dependencies": { - "cookie": "0.7.2", - "cookie-signature": "1.0.6" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/cookie-parser/node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "node_modules/cookies": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", - "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", - "dependencies": { - "depd": "~2.0.0", - "keygrip": "~1.1.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/jake": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", - "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", - "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.4", - "minimatch": "^3.1.2" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/keygrip": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", - "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", - "dependencies": { - "tsscmp": "1.0.6" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tsscmp": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", - "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", - "engines": { - "node": ">=0.6.x" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - } - } -} diff --git a/oidc/node/package.json b/oidc/node/package.json deleted file mode 100644 index f1dda7c7b..000000000 --- a/oidc/node/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "code-engine-nodejs", - "version": "1.0.0", - "description": "Simple nodejs server hosted on IBM cloud engine", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "license": "ISC", - "dependencies": { - "cookie-parser": "^1.4.7", - "ejs": "^3.1.10", - "express": "^4.21.2" - } -} diff --git a/oidc/node/public/css/style.css b/oidc/node/public/css/style.css deleted file mode 100644 index 71632b6b9..000000000 --- a/oidc/node/public/css/style.css +++ /dev/null @@ -1,13 +0,0 @@ -/* style.css */ -body,html { - background-color: #f4f4f4; - margin: 0 auto; - font-family: "IBM Plex Sans", sans-serif; - padding: 1rem; -} - -pre { - text-wrap: wrap; - word-break: break-word; - width: 95%; -} diff --git a/oidc/node/public/images/favicon.ico b/oidc/node/public/images/favicon.ico deleted file mode 100644 index 8f688bed87fc160ec873ed29f827dab6ef62b71c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2247 zcmV;&2srnNP)&H!#vjgP;#EPD z`C&5ANgRF%Iywquq7e;_8m*5J#TEsomPcP;q4ojN_TKi~d-h&yew1R7i*0Y)flOG* zP4+ow?Y+Nm@3p_Z_P)SbJB5kR4c&FWTh>)~`*?Pv#;f7wXbrQ)Lk!$Io&=onYB<@Z z2pTMky1zEnb8QhJ>8QhUL%XvL$yYziXP_4>7AJeB*m8`k&)QVB4iL zUh})+Z%{DjmW5yJzxIcGQ+0Jows@PF-W#gjNdDxzoN)dEn@m2t*}Y)<aH;zT$qa{G|KR8&aUG`3KyP@HZ!uN-&c;UYePS>vfi_Xrv ziM@D2X{>{?FXi_6UshE_z4UU~6!PMJRJ-aT5}FC(xCU>F_w;;OQ(o>5)#nXIci#fd zKXOF>X>;y*ohK4_zHC20O%&_hUweJ=Uq_R%Uf$4B z0zWw!jP_vr*UeQG6=vY`m96^<1A>Q@tM1joKtTND1}&+_wiL|$Dy@v}Us3Q$)84$L zHr~BL7@az|;E6sXO9#G}ymoLpiMv6>$@lxbJ4OW_0ElF3>GXPVa5o$Aj!c{wJV9Vn zSm{oh{DAoQ(vSBodwXKpZG*w5-v?sEO?OUwF_oZ&rYC-@kzoH$x=}PG-DeQUj`|7j zzCAY6OhH-hkyV7Q^#l{ihE6Vp4|Sh0cF#{}g2cB_bvZBy~wGEkSkek?L~#PC{d%7GB>} zsIqi9gnz;!5BRZ|JO*L+x7`gKM#aL%(wo*+$ksDQKVz+E}DnZTdD`OqY#Y_TS` zNu*lpmz8x7nP%74W_u>If;6onl0z!s{PyG7Ga83SASMpb>=b2%wTcAS6Nx?4o_{A| zWJU^p>gGd{u$2Ng~XW`=T*_@(I3o4v(~f~O$Z*H zHX(09DCN2ZY{?O;w`Kw!F;S7Ogd?eNcipG!+eQi=0JhGX`6@F#C6+Ev_Q->$mQfMP zU(<78Ku69KL;7f>A>lv=#ZtFnyDy{AFRsyEdxzh6Ykm)6G0t@Hhrexp&RVJxrn?vK zsJ}2RfxO3iFBL20VpMOJ&D)P(m;3CKW<#AU!H$f2ez%cZwzj13y)=5HR#j191nL>K zlxJA4IEg^`kG08CpPBH3+ z1=WWlB0|Rr5Ifhir70%ez!k_OiEqA7P$gCm@c`aUTO1tjn}_7oaPzu z5f;`Uc9PjMO8Wtcm3e?BB9%7Z=*h|HhSz0rxLyM|tuw(wQTP@Bf794kBdVVj)mnor zPMVzbn-Qgu=;*~asnfm=*x(9Iafe5`VflguRsn!zy;m%`!ZhcUHJ^UT2WstD-DG5# zPt6f=udr5qw6|hvXN-vZWz@$W5$d#-mI87LfHB6VNu- z??O}seub-s(Dc(1M$wIJ*NV=%+a!8F$UdiN%gL#P###WqV64U}nS?XQqR+Qoq!3)k zJ|_kG!J0wmhwliirQHxb37{Mn&ta7mp9$b+K6iL20^uzJXR7s*_tgKiaQhFO2ZXQ^ zk3jGT0AE4yI2dnYJ$e6Y;ODOGnJb#_B}e~;6nd3S+|zVd(F?;nk=-pT0Xz!AQ~=u{ zsDi?7FcQ!JlqC%SN<(W4=ws8Vaja~i6^_5x8i3|O<$+s=L3POzX$mWiC=?cxrYm(; zwn8%gks$f|mX(vXjqFgrN=jg{3cyVul))L~tpHL0>3+~O@Jxfsnf^ooljv4jeJBpE zZ>`?*I@X^0+Hq)2|E1)>K^ZL01rRx=MTBD`snx(Jr<;jmql{y|$KP{*8H3xfP>%B8 zpcTRm?e}L7>%W4b35XCL?GGFrKH0xCU7fN_BflPb!BSq{3LA?NEW diff --git a/oidc/node/views/authfailed.ejs b/oidc/node/views/authfailed.ejs deleted file mode 100644 index aba3189a1..000000000 --- a/oidc/node/views/authfailed.ejs +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - <%= pageTitle %> - - -

Authentication failed!

-
OIDC configuration properties:
-
- - ClientId=<%= clientId %> -
- AuthorizationEndpoint=<%= providerAuthorizationEndpoint %> -
- TokenEndpoint=<%= providerTokenEndpoint %> -
- UserInfoEndpoint=<%= providerUserInfoEndpoint %> -
-
-
- Login - - - - \ No newline at end of file diff --git a/oidc/node/views/index.ejs b/oidc/node/views/index.ejs deleted file mode 100644 index e15f8dd0f..000000000 --- a/oidc/node/views/index.ejs +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - <%= pageTitle %> - - - <% if(typeof user == 'object' && user?.token){ %> -

Your are authenticated!

-
OIDC configuration properties:
-
- - ClientId=<%= clientId %> -
- AuthorizationEndpoint=<%= providerAuthorizationEndpoint %> -
- TokenEndpoint=<%= providerTokenEndpoint %> -
- UserInfoEndpoint=<%= providerUserInfoEndpoint %> -
-
-
-
Access Token:
<%= user.token %>
-
-
User Profile:
<%= user.profile %>
-
- Logout - <% } else{ %> - Login - <% } %> - - - - \ No newline at end of file