implement oauth device code 'as if' endpoints

This commit is contained in:
2024-10-01 22:36:16 +02:00
parent a5d6be9338
commit 32afe88a9b
6 changed files with 92 additions and 5 deletions

View File

@@ -1,5 +1,5 @@
# Utiliser une image officielle de Go pour construire l'application
FROM golang:1.20-alpine AS builder
FROM golang:1.23-alpine AS builder
# Installer les dépendances pour PostgreSQL
RUN apk add --no-cache git

View File

@@ -4,5 +4,7 @@ metadata:
name: {{ include "webapp.name" . }}-config
namespace: {{ .Release.Namespace }}
data:
OAUTH_ALLOWED_HTTP2_AUTHORITY: webapp.arcodange.duckdns.org
OAUTH_DEVICE_CODE_ALLOWED_IPS: 90.16.102.250,
DATABASE_URL: postgres://pgbouncer_auth:pgbouncer_auth@pgbouncer.tools/postgres?sslmode=disable
# DATABASE_URL: postgres://username:password@localhost/dbname?sslmode=disable

View File

@@ -104,7 +104,8 @@ volumeMounts: []
# mountPath: "/etc/foo"
# readOnly: true
nodeSelector: {}
nodeSelector:
kubernetes.io/hostname: pi1 # entrypoint of the network, allows to avoid NAT and keep user IP
tolerations: []

7
go.mod
View File

@@ -1,5 +1,8 @@
module gitea.arcodange.duckdns.org/arcodange-org/webapp
go 1.20
go 1.23
require github.com/lib/pq v1.10.9 // indirect
require (
github.com/lib/pq v1.10.9 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
)

2
go.sum
View File

@@ -1,2 +1,4 @@
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=

81
main.go
View File

@@ -5,15 +5,25 @@ import (
"fmt"
"html/template"
"log"
"net"
"net/http"
"os"
"slices"
"strconv"
"strings"
"time"
"github.com/patrickmn/go-cache"
_ "github.com/lib/pq" // PostgreSQL driver
)
var db *sql.DB // Global database connection
var (
db *sql.DB // Global database connection
c = cache.New(5*time.Minute, 10*time.Minute)
oauthAllowedHttp2Authority = os.Getenv("OAUTH_ALLOWED_HTTP2_AUTHORITY") // URL authorized for device code
oauthDeviceCodeAllowedIPs = strings.Split(os.Getenv("OAUTH_DEVICE_CODE_ALLOWED_IPS"), ",") // IPS autorisées pour /retrieve
)
// dbConnection initializes the database connection.
func dbConnection() (*sql.DB, error) {
@@ -137,11 +147,26 @@ type CallbackData struct {
// oauth2_callback handles HTTP requests and display a message according to queryParams
func oauth2_callback(w http.ResponseWriter, r *http.Request) {
// Vérifier le référent (ou origine)
authorityHeader := r.Header.Get(":authority")
if oauthAllowedHttp2Authority != "" && authorityHeader != oauthAllowedHttp2Authority {
fmt.Println(":authority: "+authorityHeader)
http.Error(w, "Access denied: invalid referer or origin", http.StatusForbidden)
return
}
// Récupération des paramètres query
queryParams := r.URL.Query()
code := queryParams.Get("code")
state := queryParams.Get("state")
// Stocker state et code dans le cache avec TTL de 5 minutes
if state != "" && code != "" {
c.Set(state, code, cache.DefaultExpiration)
}
// Construction d'une map pour les autres paramètres
otherParams := make(map[string]string)
for key, values := range queryParams {
@@ -250,6 +275,46 @@ func oauth2_callback(w http.ResponseWriter, r *http.Request) {
}
}
// Handler pour récupérer un code à partir d'un state avec une restriction d'IP
func retrieveHandler(w http.ResponseWriter, r *http.Request) {
// Récupérer l'IP de l'utilisateur
userIP, _, err := net.SplitHostPort(r.RemoteAddr)
userIPforwarded := r.Header.Get("X-Forwarded-For")
if err != nil ||
!slices.Contains(oauthDeviceCodeAllowedIPs, userIP) &&
!slices.Contains(oauthDeviceCodeAllowedIPs, userIPforwarded) {
fmt.Fprintln(os.Stderr, "denied userIP: "+userIP)
// Parcourir tous les headers
for name, values := range r.Header {
// name représente le nom de l'en-tête
// values est une slice contenant toutes les valeurs associées à cet en-tête
for _, value := range values {
fmt.Fprintf(os.Stderr,"%s: %s\n", name, value)
}
}
http.Error(w, "Access denied: invalid IP", http.StatusForbidden)
return
}
// Récupérer le paramètre `state` depuis l'URL
state := r.URL.Query().Get("state")
if state == "" {
http.Error(w, "State parameter is required", http.StatusBadRequest)
return
}
// Vérifier si le state existe dans le cache
code, found := c.Get(state)
if !found {
http.Error(w, "State not found or expired", http.StatusNotFound)
return
}
// Retourner le code associé au state
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, code)
}
func test_oauth2_callback(w http.ResponseWriter, r *http.Request) {
html := `
<html>
@@ -325,12 +390,26 @@ func main() {
// Define the handler for the `/readiness` probe
http.HandleFunc("/readiness", readinessHandler)
/*
Gitea doesn't come with device flow # https://github.com/go-gitea/gitea/issues/27309
https://gitea.arcodange.duckdns.org/.well-known/openid-configuration
"grant_types_supported": [
"authorization_code",
"refresh_token"
]
So we can use the authorization_code and redirect to this endpoint
and then the client can poll for the code matching the state it chose
*/
http.HandleFunc("/oauth-callback", oauth2_callback)
// Define the handler to exchange a state for a code
http.HandleFunc("/retrieve", retrieveHandler)
http.HandleFunc("/test-oauth-callback", test_oauth2_callback)
// Start the HTTP server
port := ":8080"
log.Printf("Server starting on port %s\n", port)
fmt.Println("new version indeed")
err = http.ListenAndServe(port, nil)
if err != nil {
log.Fatalf("Server failed to start: %v", err)