feat: split oauth endpoints into files
This commit is contained in:
53
internal/oauth/code.go
Normal file
53
internal/oauth/code.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
36
internal/oauth/jwks.go
Normal file
36
internal/oauth/jwks.go
Normal file
@ -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)
|
||||
}
|
31
internal/oauth/openid.go
Normal file
31
internal/oauth/openid.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
// }
|
||||
// }
|
||||
|
129
internal/oauth/token.go
Normal file
129
internal/oauth/token.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user