diff --git a/internal/oauth/code.go b/internal/oauth/code.go new file mode 100644 index 0000000..623f786 --- /dev/null +++ b/internal/oauth/code.go @@ -0,0 +1,53 @@ +package oauth + +import ( + "encoding/json" + "fmt" + "net/http" + + "gitea.local/admin/hspguard/internal/util" + "gitea.local/admin/hspguard/internal/web" + "github.com/google/uuid" +) + +func (h *OAuthHandler) getAuthCode(w http.ResponseWriter, r *http.Request) { + userId, ok := util.GetRequestUserId(r.Context()) + if !ok { + web.Error(w, "failed to get user id from auth session", http.StatusInternalServerError) + return + } + + user, err := h.repo.FindUserId(r.Context(), uuid.MustParse(userId)) + if err != nil { + web.Error(w, "user with provided id does not exist", http.StatusUnauthorized) + return + } + + type Request struct { + Nonce string `json:"nonce"` + } + + var req Request + + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&req); err != nil { + web.Error(w, "nonce field is required in request", http.StatusBadRequest) + return + } + + // TODO: Create real authorization code + + type Response struct { + Code string `json:"code"` + } + + encoder := json.NewEncoder(w) + + w.Header().Set("Content-Type", "application/json") + + if err := encoder.Encode(Response{ + Code: fmt.Sprintf("%s,%s", user.ID.String(), req.Nonce), + }); err != nil { + web.Error(w, "failed to encode response", http.StatusInternalServerError) + } +} diff --git a/internal/oauth/jwks.go b/internal/oauth/jwks.go new file mode 100644 index 0000000..a3b5728 --- /dev/null +++ b/internal/oauth/jwks.go @@ -0,0 +1,36 @@ +package oauth + +import ( + "encoding/base64" + "encoding/json" + "net/http" + + "gitea.local/admin/hspguard/internal/util" + "gitea.local/admin/hspguard/internal/web" +) + +func (h *OAuthHandler) WriteJWKS(w http.ResponseWriter, r *http.Request) { + pubKey, err := util.ParseBase64PublicKey(h.cfg.Jwt.PublicKey) + if err != nil { + web.Error(w, "failed to parse public key", http.StatusInternalServerError) + } + + n := base64.RawURLEncoding.EncodeToString(pubKey.N.Bytes()) + e := base64.RawURLEncoding.EncodeToString([]byte{1, 0, 1}) // 65537 = 0x010001 + + jwks := map[string]interface{}{ + "keys": []map[string]string{ + { + "kty": "RSA", + "kid": "my-rsa-key-1", + "use": "sig", + "alg": "RS256", + "n": n, + "e": e, + }, + }, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(jwks) +} diff --git a/internal/oauth/openid.go b/internal/oauth/openid.go new file mode 100644 index 0000000..aaa863d --- /dev/null +++ b/internal/oauth/openid.go @@ -0,0 +1,31 @@ +package oauth + +import ( + "encoding/json" + "net/http" + + "gitea.local/admin/hspguard/internal/web" +) + +func (h *OAuthHandler) OpenIdConfiguration(w http.ResponseWriter, r *http.Request) { + type Response struct { + TokenEndpoint string `json:"token_endpoint"` + AuthorizationEndpoint string `json:"authorization_endpoint"` + JwksURI string `json:"jwks_uri"` + Issuer string `json:"issuer"` + EndSessionEndpoint string `json:"end_session_endpoint"` + } + + w.Header().Set("Content-Type", "application/json") + + encoder := json.NewEncoder(w) + if err := encoder.Encode(Response{ + TokenEndpoint: h.cfg.Jwt.Issuer + "/api/v1/oauth/token", + AuthorizationEndpoint: h.cfg.Jwt.Issuer + "/auth", + JwksURI: h.cfg.Jwt.Issuer + "/.well-known/jwks.json", + Issuer: h.cfg.Jwt.Issuer, + EndSessionEndpoint: h.cfg.Jwt.Issuer + "/api/v1/oauth/logout", + }); err != nil { + web.Error(w, "failed to encode response", http.StatusInternalServerError) + } +} diff --git a/internal/oauth/routes.go b/internal/oauth/routes.go index 107f2d1..31d9626 100644 --- a/internal/oauth/routes.go +++ b/internal/oauth/routes.go @@ -1,22 +1,9 @@ package oauth import ( - "encoding/base64" - "encoding/json" - "fmt" - "log" - "net/http" - "strings" - "time" - "gitea.local/admin/hspguard/internal/config" "gitea.local/admin/hspguard/internal/repository" - "gitea.local/admin/hspguard/internal/types" - "gitea.local/admin/hspguard/internal/util" - "gitea.local/admin/hspguard/internal/web" "github.com/go-chi/chi/v5" - "github.com/golang-jwt/jwt/v5" - "github.com/google/uuid" ) type OAuthHandler struct { @@ -38,226 +25,3 @@ func (h *OAuthHandler) RegisterRoutes(router chi.Router) { r.Post("/code", h.getAuthCode) }) } - -func (h *OAuthHandler) WriteJWKS(w http.ResponseWriter, r *http.Request) { - pubKey, err := util.ParseBase64PublicKey(h.cfg.Jwt.PublicKey) - if err != nil { - web.Error(w, "failed to parse public key", http.StatusInternalServerError) - } - - n := base64.RawURLEncoding.EncodeToString(pubKey.N.Bytes()) - e := base64.RawURLEncoding.EncodeToString([]byte{1, 0, 1}) // 65537 = 0x010001 - - jwks := map[string]interface{}{ - "keys": []map[string]string{ - { - "kty": "RSA", - "kid": "my-rsa-key-1", - "use": "sig", - "alg": "RS256", - "n": n, - "e": e, - }, - }, - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(jwks) -} - -func (h *OAuthHandler) OpenIdConfiguration(w http.ResponseWriter, r *http.Request) { - type Response struct { - TokenEndpoint string `json:"token_endpoint"` - AuthorizationEndpoint string `json:"authorization_endpoint"` - JwksURI string `json:"jwks_uri"` - Issuer string `json:"issuer"` - EndSessionEndpoint string `json:"end_session_endpoint"` - } - - w.Header().Set("Content-Type", "application/json") - - encoder := json.NewEncoder(w) - if err := encoder.Encode(Response{ - TokenEndpoint: h.cfg.Jwt.Issuer + "/api/v1/oauth/token", - AuthorizationEndpoint: h.cfg.Jwt.Issuer + "/auth", - JwksURI: h.cfg.Jwt.Issuer + "/.well-known/jwks.json", - Issuer: h.cfg.Jwt.Issuer, - EndSessionEndpoint: h.cfg.Jwt.Issuer + "/api/v1/oauth/logout", - }); err != nil { - web.Error(w, "failed to encode response", http.StatusInternalServerError) - } -} - -func (h *OAuthHandler) getAuthCode(w http.ResponseWriter, r *http.Request) { - userId, ok := util.GetRequestUserId(r.Context()) - if !ok { - web.Error(w, "failed to get user id from auth session", http.StatusInternalServerError) - return - } - - user, err := h.repo.FindUserId(r.Context(), uuid.MustParse(userId)) - if err != nil { - web.Error(w, "user with provided id does not exist", http.StatusUnauthorized) - return - } - - type Request struct { - Nonce string `json:"nonce"` - } - - var req Request - - decoder := json.NewDecoder(r.Body) - if err := decoder.Decode(&req); err != nil { - web.Error(w, "nonce field is required in request", http.StatusBadRequest) - return - } - - // TODO: Create real authorization code - - type Response struct { - Code string `json:"code"` - } - - encoder := json.NewEncoder(w) - - w.Header().Set("Content-Type", "application/json") - - if err := encoder.Encode(Response{ - Code: fmt.Sprintf("%s,%s", user.ID.String(), req.Nonce), - }); err != nil { - web.Error(w, "failed to encode response", http.StatusInternalServerError) - } -} - -func (h *OAuthHandler) tokenEndpoint(w http.ResponseWriter, r *http.Request) { - log.Println("[OAUTH] New request to token endpoint") - - authHeader := r.Header.Get("Authorization") - if authHeader == "" || !strings.HasPrefix(authHeader, "Basic ") { - w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - // Decode credentials - payload, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(authHeader, "Basic ")) - if err != nil { - http.Error(w, "Invalid auth encoding", http.StatusBadRequest) - return - } - - var clientId string - var clientSecret string - - parts := strings.SplitN(string(payload), ":", 2) - if len(parts) != 2 { - http.Error(w, "Unauthorized", http.StatusForbidden) - return - } - - clientId = parts[0] - clientSecret = parts[1] - - log.Printf("Some client is trying to exchange code with id: %s and secret: %s\n", clientId, clientSecret) - - // Parse the form data - err = r.ParseForm() - if err != nil { - http.Error(w, "Failed to parse form", http.StatusBadRequest) - return - } - - grantType := r.FormValue("grant_type") - redirectUri := r.FormValue("redirect_uri") - - log.Printf("Redirect URI is %s\n", redirectUri) - - switch grantType { - case "authorization_code": - code := r.FormValue("code") - - fmt.Printf("Code received: %s\n", code) - - // TODO: Verify code from another db table - nonce := strings.Split(code, ",")[1] - - userId := strings.Split(code, ",")[0] - - user, err := h.repo.FindUserId(r.Context(), uuid.MustParse(userId)) - if err != nil { - web.Error(w, "requested user not found", http.StatusNotFound) - return - } - - claims := types.ApiClaims{ - Email: user.Email, - // TODO: - EmailVerified: true, - Name: user.FullName, - Picture: user.ProfilePicture.String, - Nonce: nonce, - Roles: []string{"user", "admin"}, - RegisteredClaims: jwt.RegisteredClaims{ - Issuer: h.cfg.Jwt.Issuer, - // TODO: use dedicated API id that is in local DB and bind to user there - Subject: user.ID.String(), - Audience: jwt.ClaimStrings{clientId}, - IssuedAt: jwt.NewNumericDate(time.Now()), - ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)), - }, - } - - idToken, err := util.SignJwtToken(claims, h.cfg.Jwt.PrivateKey) - if err != nil { - web.Error(w, "failed to sign id token", http.StatusInternalServerError) - return - } - - type Response struct { - IdToken string `json:"id_token"` - TokenType string `json:"token_type"` - AccessToken string `json:"access_token"` - Email string `json:"email"` - // TODO: add expires_in, refresh_token, scope (RFC 8693 $2) - } - - response := Response{ - IdToken: idToken, - TokenType: "Bearer", - // FIXME: - AccessToken: idToken, - Email: user.Email, - } - - log.Printf("sending following response: %#v\n", response) - - w.Header().Set("Content-Type", "application/json") - encoder := json.NewEncoder(w) - if err := encoder.Encode(response); err != nil { - web.Error(w, "failed to encode response", http.StatusInternalServerError) - } - default: - web.Error(w, "unsupported grant type", http.StatusBadRequest) - } -} - -// func (h *OAuthHandler) Metadata(w http.ResponseWriter, r *http.Request) { -// type Response struct { -// TokenEndpoint string `json:"token_endpoint"` -// AuthEndpoint string `json:"authorization_endpoint"` -// Issuer string `json:"issuer"` -// JwksUri string `json:"jwks_uri"` -// } - -// encoder := json.NewEncoder(w) - -// if err := encoder.Encode(Response{ -// TokenEndpoint: "http://192.168.178.21:3001/api/v1/oauth/token", -// AuthEndpoint: "https://147f-2a00-10-5b00-c801-4882-7ef0-5e68-d360.ngrok-free.app/authorize", -// JwksUri: "http://192.168.178.21:3001/.well-known/jwks.json", -// Issuer: "http://192.168.178.21:3001", -// }); err != nil { -// web.Error(w, "failed to encode response", http.StatusInternalServerError) -// } -// } diff --git a/internal/oauth/token.go b/internal/oauth/token.go new file mode 100644 index 0000000..feef0bb --- /dev/null +++ b/internal/oauth/token.go @@ -0,0 +1,129 @@ +package oauth + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "log" + "net/http" + "strings" + "time" + + "gitea.local/admin/hspguard/internal/types" + "gitea.local/admin/hspguard/internal/util" + "gitea.local/admin/hspguard/internal/web" + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" +) + +func (h *OAuthHandler) tokenEndpoint(w http.ResponseWriter, r *http.Request) { + log.Println("[OAUTH] New request to token endpoint") + + authHeader := r.Header.Get("Authorization") + if authHeader == "" || !strings.HasPrefix(authHeader, "Basic ") { + w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + // Decode credentials + payload, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(authHeader, "Basic ")) + if err != nil { + http.Error(w, "Invalid auth encoding", http.StatusBadRequest) + return + } + + var clientId string + var clientSecret string + + parts := strings.SplitN(string(payload), ":", 2) + if len(parts) != 2 { + http.Error(w, "Unauthorized", http.StatusForbidden) + return + } + + clientId = parts[0] + clientSecret = parts[1] + + log.Printf("Some client is trying to exchange code with id: %s and secret: %s\n", clientId, clientSecret) + + // Parse the form data + err = r.ParseForm() + if err != nil { + http.Error(w, "Failed to parse form", http.StatusBadRequest) + return + } + + grantType := r.FormValue("grant_type") + redirectUri := r.FormValue("redirect_uri") + + log.Printf("Redirect URI is %s\n", redirectUri) + + switch grantType { + case "authorization_code": + code := r.FormValue("code") + + fmt.Printf("Code received: %s\n", code) + + // TODO: Verify code from another db table + nonce := strings.Split(code, ",")[1] + + userId := strings.Split(code, ",")[0] + + user, err := h.repo.FindUserId(r.Context(), uuid.MustParse(userId)) + if err != nil { + web.Error(w, "requested user not found", http.StatusNotFound) + return + } + + claims := types.ApiClaims{ + Email: user.Email, + // TODO: + EmailVerified: true, + Name: user.FullName, + Picture: user.ProfilePicture.String, + Nonce: nonce, + Roles: []string{"user", "admin"}, + RegisteredClaims: jwt.RegisteredClaims{ + Issuer: h.cfg.Jwt.Issuer, + // TODO: use dedicated API id that is in local DB and bind to user there + Subject: user.ID.String(), + Audience: jwt.ClaimStrings{clientId}, + IssuedAt: jwt.NewNumericDate(time.Now()), + ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)), + }, + } + + idToken, err := util.SignJwtToken(claims, h.cfg.Jwt.PrivateKey) + if err != nil { + web.Error(w, "failed to sign id token", http.StatusInternalServerError) + return + } + + type Response struct { + IdToken string `json:"id_token"` + TokenType string `json:"token_type"` + AccessToken string `json:"access_token"` + Email string `json:"email"` + // TODO: add expires_in, refresh_token, scope (RFC 8693 $2) + } + + response := Response{ + IdToken: idToken, + TokenType: "Bearer", + // FIXME: + AccessToken: idToken, + Email: user.Email, + } + + log.Printf("sending following response: %#v\n", response) + + w.Header().Set("Content-Type", "application/json") + encoder := json.NewEncoder(w) + if err := encoder.Encode(response); err != nil { + web.Error(w, "failed to encode response", http.StatusInternalServerError) + } + default: + web.Error(w, "unsupported grant type", http.StatusBadRequest) + } +}