diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 0d20de8..cd89829 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -17,6 +17,7 @@ import { AppContextProvider } from "./context/app-context.tsx"; import { UserContextProvider } from "./context/user-context.tsx"; import { Toaster } from "@/components/ui/sonner"; import { ThemeProvider } from "./components/providers/theme-provider.tsx"; +import { AuthorizePage } from "./pages/authorize-page.tsx"; const queryClient = new QueryClient(); @@ -31,6 +32,7 @@ createRoot(document.getElementById("root")!).render( } errorElement={}> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/pages/authorize-page.tsx b/frontend/src/pages/authorize-page.tsx new file mode 100644 index 0000000..6befa96 --- /dev/null +++ b/frontend/src/pages/authorize-page.tsx @@ -0,0 +1,99 @@ +import { useUserContext } from "@/context/user-context"; +import { useQuery } from "@tanstack/react-query"; +import { Navigate } from "react-router"; +import { useLocation } from "react-router"; +import { + Card, + CardHeader, + CardTitle, + CardDescription, + CardFooter, +} from "@/components/ui/card"; +import { getOidcClientInfoScehma } from "@/schemas/oidc-schemas"; +import { Button } from "@/components/ui/button"; + +type AuthorizePageProps = { + scope: string; + responseType: string; + clientId: string; + redirectUri: string; + state: string; +}; + +const optionalAuthorizeProps = ["state"]; + +export const AuthorizePage = () => { + const { isLoggedIn } = useUserContext(); + const { search } = useLocation(); + + const searchParams = new URLSearchParams(search); + + // If there is a better way to do this, please do let me know + const props: AuthorizePageProps = { + scope: searchParams.get("scope") || "", + responseType: searchParams.get("response_type") || "", + clientId: searchParams.get("client_id") || "", + redirectUri: searchParams.get("redirect_uri") || "", + state: searchParams.get("state") || "", + }; + + const getClientInfo = useQuery({ + queryKey: ["client", props.clientId], + queryFn: async () => { + const res = await fetch(`/api/oidc/clients/${props.clientId}`); + const data = await getOidcClientInfoScehma.parseAsync(await res.json()); + return data; + }, + }); + + if (!isLoggedIn) { + // TODO: Pass the params to the login page, so user can login -> authorize + return ; + } + + for (const key in Object.keys(props)) { + if ( + !props[key as keyof AuthorizePageProps] && + !optionalAuthorizeProps.includes(key) + ) { + // TODO: Add reason for error + return ; + } + } + + if (getClientInfo.isLoading) { + return ( + + + Loading... + + Please wait while we load the client information. + + + + ); + } + + if (getClientInfo.isError) { + // TODO: Add reason for error + return ; + } + + return ( + + + + Continue to {getClientInfo.data?.name || "Unknown"}? + + + Would you like to continue to this app? Please keep in mind that this + app will have access to your email and other information. + + + + + + + + ); +}; diff --git a/frontend/src/schemas/oidc-schemas.ts b/frontend/src/schemas/oidc-schemas.ts new file mode 100644 index 0000000..853745c --- /dev/null +++ b/frontend/src/schemas/oidc-schemas.ts @@ -0,0 +1,5 @@ +import { z } from "zod"; + +export const getOidcClientInfoScehma = z.object({ + name: z.string(), +}); diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index f1c4b0b..e9cdd5a 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -30,6 +30,7 @@ type BootstrapApp struct { users []config.User oauthProviders map[string]config.OAuthServiceConfig configuredProviders []controller.Provider + oidcClients []config.OIDCClientConfig } services Services } @@ -84,6 +85,12 @@ func (app *BootstrapApp) Setup() error { app.context.oauthProviders[id] = provider } + // Setup OIDC clients + for id, client := range app.config.OIDC.Clients { + client.ID = id + app.context.oidcClients = append(app.context.oidcClients, client) + } + // Get cookie domain cookieDomain, err := utils.GetCookieDomain(app.config.AppURL) diff --git a/internal/bootstrap/router_bootstrap.go b/internal/bootstrap/router_bootstrap.go index f96670e..c854c45 100644 --- a/internal/bootstrap/router_bootstrap.go +++ b/internal/bootstrap/router_bootstrap.go @@ -86,6 +86,12 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) { oauthController.SetupRoutes() + oidcController := controller.NewOIDCController(controller.OIDCControllerConfig{ + Clients: app.context.oidcClients, + }, apiRouter) + + oidcController.SetupRoutes() + proxyController := controller.NewProxyController(controller.ProxyControllerConfig{ AppURL: app.config.AppURL, }, apiRouter, app.services.accessControlService, app.services.authService) diff --git a/internal/config/config.go b/internal/config/config.go index 16ad292..de87390 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -132,6 +132,7 @@ type OAuthServiceConfig struct { } type OIDCClientConfig struct { + ID string `description:"OIDC client ID." yaml:"-"` ClientID string `description:"OIDC client ID." yaml:"clientId"` ClientSecret string `description:"OIDC client secret." yaml:"clientSecret"` ClientSecretFile string `description:"Path to the file containing the OIDC client secret." yaml:"clientSecretFile"` diff --git a/internal/controller/oidc_controller.go b/internal/controller/oidc_controller.go new file mode 100644 index 0000000..8fbf2ce --- /dev/null +++ b/internal/controller/oidc_controller.go @@ -0,0 +1,71 @@ +package controller + +import ( + "github.com/gin-gonic/gin" + "github.com/steveiliop56/tinyauth/internal/config" + "github.com/steveiliop56/tinyauth/internal/utils/tlog" +) + +type OIDCControllerConfig struct { + Clients []config.OIDCClientConfig +} + +type OIDCController struct { + clients []config.OIDCClientConfig + router *gin.RouterGroup +} + +func NewOIDCController(config OIDCControllerConfig, router *gin.RouterGroup) *OIDCController { + return &OIDCController{ + clients: config.Clients, + router: router, + } +} + +func (controller *OIDCController) SetupRoutes() { + oidcGroup := controller.router.Group("/oidc") + oidcGroup.GET("/clients/:id", controller.GetClientInfo) +} + +type ClientRequest struct { + ClientID string `uri:"id" binding:"required"` +} + +func (controller *OIDCController) GetClientInfo(c *gin.Context) { + var req ClientRequest + + err := c.BindUri(&req) + if err != nil { + tlog.App.Error().Err(err).Msg("Failed to bind URI") + c.JSON(400, gin.H{ + "status": 400, + "message": "Bad Request", + }) + return + } + + var client *config.OIDCClientConfig + + // Inefficient yeah, but it will be good until we have thousands of clients + for _, clientCfg := range controller.clients { + if clientCfg.ClientID == req.ClientID { + client = &clientCfg + break + } + } + + if client == nil { + tlog.App.Warn().Str("client_id", req.ClientID).Msg("Client not found") + c.JSON(404, gin.H{ + "status": 404, + "message": "Client not found", + }) + return + } + + c.JSON(200, gin.H{ + "status": 200, + "client": &client.ClientID, + "name": &client.Name, + }) +}