Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -31,6 +32,7 @@ createRoot(document.getElementById("root")!).render(
<Route element={<Layout />} errorElement={<ErrorPage />}>
<Route path="/" element={<App />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/authorize" element={<AuthorizePage />} />
<Route path="/logout" element={<LogoutPage />} />
<Route path="/continue" element={<ContinuePage />} />
<Route path="/totp" element={<TotpPage />} />
Expand Down
139 changes: 139 additions & 0 deletions frontend/src/pages/authorize-page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { useUserContext } from "@/context/user-context";
import { useMutation, useQuery } from "@tanstack/react-query";
import { Navigate, useNavigate } 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";
import axios from "axios";
import { toast } from "sonner";

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 navigate = useNavigate();

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;
},
});
Comment thread
steveiliop56 marked this conversation as resolved.

const authorizeMutation = useMutation({
mutationFn: () => {
return axios.post("/api/oidc/authorize", {
scope: props.scope,
response_type: props.responseType,
client_id: props.clientId,
redirect_uri: props.redirectUri,
state: props.state,
});
},
mutationKey: ["authorize", props.clientId],
onSuccess: (data) => {
toast.info("Authorized", {
description: "You will be soon redirected to your application",
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
window.location.replace(
`${data.data.redirect_uri}?code=${encodeURIComponent(data.data.code)}&state=${encodeURIComponent(data.data.state)}`,
);
},
onError: (error) => {
window.location.replace(
`/error?error=${encodeURIComponent(error.message)}`,
);
},
});

if (!isLoggedIn) {
// TODO: Pass the params to the login page, so user can login -> authorize
return <Navigate to="/login" replace />;
}

Object.keys(props).forEach((key) => {
if (
!props[key as keyof AuthorizePageProps] &&
!optionalAuthorizeProps.includes(key)
) {
// TODO: Add reason for error
return <Navigate to="/error" replace />;
}
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

if (getClientInfo.isLoading) {
return (
<Card className="min-w-xs sm:min-w-sm">
<CardHeader>
<CardTitle className="text-3xl">Loading...</CardTitle>
<CardDescription>
Please wait while we load the client information.
</CardDescription>
</CardHeader>
</Card>
);
}

if (getClientInfo.isError) {
// TODO: Add reason for error
return <Navigate to="/error" replace />;
}
Comment thread
steveiliop56 marked this conversation as resolved.

return (
<Card className="min-w-xs sm:min-w-sm">
<CardHeader>
<CardTitle className="text-3xl">
Continue to {getClientInfo.data?.name || "Unknown"}?
</CardTitle>
<CardDescription>
Would you like to continue to this app? Please keep in mind that this
app will have access to your email and other information.
</CardDescription>
</CardHeader>
<CardFooter className="flex flex-col items-stretch gap-2">
<Button
onClick={() => authorizeMutation.mutate()}
loading={authorizeMutation.isPending}
>
Authorize
</Button>
<Button
onClick={() => navigate("/")}
disabled={authorizeMutation.isPending}
variant="outline"
>
Cancel
</Button>
</CardFooter>
</Card>
);
};
5 changes: 5 additions & 0 deletions frontend/src/schemas/oidc-schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { z } from "zod";

export const getOidcClientInfoScehma = z.object({
name: z.string(),
});
Comment thread
steveiliop56 marked this conversation as resolved.
Outdated
3 changes: 3 additions & 0 deletions internal/assets/migrations/000005_oidc_session.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
DROP TABLE IF EXISTS "oidc_tokens";
DROP TABLE IF EXISTS "oidc_userinfo";
DROP TABLE IF EXISTS "oidc_codes";
25 changes: 25 additions & 0 deletions internal/assets/migrations/000005_oidc_session.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
CREATE TABLE IF NOT EXISTS "oidc_codes" (
"sub" TEXT NOT NULL UNIQUE,
"code" TEXT NOT NULL PRIMARY KEY UNIQUE,
"scope" TEXT NOT NULL,
"redirect_uri" TEXT NOT NULL,
"client_id" TEXT NOT NULL,
"expires_at" INTEGER NOT NULL
);

CREATE TABLE IF NOT EXISTS "oidc_tokens" (
"sub" TEXT NOT NULL UNIQUE,
"access_token" TEXT NOT NULL PRIMARY KEY UNIQUE,
"scope" TEXT NOT NULL,
"client_id" TEXT NOT NULL,
"expires_at" INTEGER NOT NULL
);

CREATE TABLE IF NOT EXISTS "oidc_userinfo" (
"sub" TEXT NOT NULL UNIQUE PRIMARY KEY,
"name" TEXT NOT NULL,
"preferred_username" TEXT NOT NULL,
"email" TEXT NOT NULL,
"groups" TEXT NOT NULL,
"updated_at" INTEGER NOT NULL
);
9 changes: 8 additions & 1 deletion internal/bootstrap/app_bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type BootstrapApp struct {
users []config.User
oauthProviders map[string]config.OAuthServiceConfig
configuredProviders []controller.Provider
oidcClients []config.OIDCClientConfig
}
services Services
}
Expand Down Expand Up @@ -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)
}
Comment thread
steveiliop56 marked this conversation as resolved.

// Get cookie domain
cookieDomain, err := utils.GetCookieDomain(app.config.AppURL)

Expand Down Expand Up @@ -169,7 +176,7 @@ func (app *BootstrapApp) Setup() error {
app.context.configuredProviders = configuredProviders

// Setup router
router, err := app.setupRouter()
router, err := app.setupRouter(queries)

if err != nil {
return fmt.Errorf("failed to setup routes: %w", err)
Expand Down
10 changes: 9 additions & 1 deletion internal/bootstrap/router_bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import (
"github.com/steveiliop56/tinyauth/internal/config"
"github.com/steveiliop56/tinyauth/internal/controller"
"github.com/steveiliop56/tinyauth/internal/middleware"
"github.com/steveiliop56/tinyauth/internal/repository"

"github.com/gin-gonic/gin"
)

var DEV_MODES = []string{"main", "test", "development"}

func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
func (app *BootstrapApp) setupRouter(queries *repository.Queries) (*gin.Engine, error) {
if !slices.Contains(DEV_MODES, config.Version) {
gin.SetMode(gin.ReleaseMode)
}
Expand Down Expand Up @@ -86,6 +87,13 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {

oauthController.SetupRoutes()

oidcController := controller.NewOIDCController(controller.OIDCControllerConfig{
Clients: app.context.oidcClients,
AppURL: app.config.AppURL,
}, apiRouter, queries)

oidcController.SetupRoutes()

proxyController := controller.NewProxyController(controller.ProxyControllerConfig{
AppURL: app.config.AppURL,
}, apiRouter, app.services.accessControlService, app.services.authService)
Expand Down
34 changes: 24 additions & 10 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Config struct {
Auth AuthConfig `description:"Authentication configuration." yaml:"auth"`
Apps map[string]App `description:"Application ACLs configuration." yaml:"apps"`
OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth"`
OIDC OIDCConfig `description:"OIDC configuration." yaml:"oidc"`
UI UIConfig `description:"UI customization." yaml:"ui"`
Ldap LdapConfig `description:"LDAP configuration." yaml:"ldap"`
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"`
Expand Down Expand Up @@ -60,6 +61,10 @@ type OAuthConfig struct {
Providers map[string]OAuthServiceConfig `description:"OAuth providers configuration." yaml:"providers"`
}

type OIDCConfig struct {
Clients map[string]OIDCClientConfig `description:"OIDC clients configuration." yaml:"clients"`
}

type UIConfig struct {
Title string `description:"The title of the UI." yaml:"title"`
ForgotPasswordMessage string `description:"Message displayed on the forgot password page." yaml:"forgotPasswordMessage"`
Expand Down Expand Up @@ -114,16 +119,25 @@ type Claims struct {
}

type OAuthServiceConfig struct {
ClientID string `description:"OAuth client ID."`
ClientSecret string `description:"OAuth client secret."`
ClientSecretFile string `description:"Path to the file containing the OAuth client secret."`
Scopes []string `description:"OAuth scopes."`
RedirectURL string `description:"OAuth redirect URL."`
AuthURL string `description:"OAuth authorization URL."`
TokenURL string `description:"OAuth token URL."`
UserinfoURL string `description:"OAuth userinfo URL."`
Insecure bool `description:"Allow insecure OAuth connections."`
Name string `description:"Provider name in UI."`
ClientID string `description:"OAuth client ID." yaml:"clientId"`
ClientSecret string `description:"OAuth client secret." yaml:"clientSecret"`
ClientSecretFile string `description:"Path to the file containing the OAuth client secret." yaml:"clientSecretFile"`
Scopes []string `description:"OAuth scopes." yaml:"scopes"`
RedirectURL string `description:"OAuth redirect URL." yaml:"redirectUrl"`
AuthURL string `description:"OAuth authorization URL." yaml:"authUrl"`
TokenURL string `description:"OAuth token URL." yaml:"tokenUrl"`
UserinfoURL string `description:"OAuth userinfo URL." yaml:"userinfoUrl"`
Insecure bool `description:"Allow insecure OAuth connections." yaml:"insecure"`
Name string `description:"Provider name in UI." yaml:"name"`
}

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"`
TrustedRedirectURLs []string `description:"List of trusted redirect URLs." yaml:"trustedRedirectUrls"`
Name string `description:"Client name in UI." yaml:"name"`
}

var OverrideProviders = map[string]string{
Expand Down
Loading