From fb48f1eb2d2021554a1cbdd1e1baee08e2ca65d4 Mon Sep 17 00:00:00 2001 From: Stavros Date: Fri, 3 Jul 2026 23:55:22 +0300 Subject: [PATCH] feat: add swagger comments for context, health, oauth and oidc controllers --- Makefile | 8 +- internal/bootstrap/router_bootstrap.go | 12 +- internal/controller/context_controller.go | 16 + internal/controller/controller.go | 4 + internal/controller/health_controller.go | 15 +- internal/controller/oauth_controller.go | 83 +- internal/controller/oidc_controller.go | 208 +++-- internal/swagger/docs.go | 1034 ++++++++++++++++++++- internal/swagger/swagger.json | 1034 ++++++++++++++++++++- internal/swagger/swagger.yaml | 683 +++++++++++++- 10 files changed, 2997 insertions(+), 100 deletions(-) diff --git a/Makefile b/Makefile index a3cd28de..99cd9188 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ PROD_COMPOSE := $(shell test -f "docker-compose.test.prod.yml" && echo "docker-c .DEFAULT_GOAL := binary -.PHONY: deps clean-data clean-webui webui binary binary-linux-amd64 binary-linux-arm64 test vet test-race dev dev-infisical prod prod-infisical sql generate docker docker-distroless swagger +.PHONY: deps clean-data clean-webui webui binary binary-linux-amd64 binary-linux-arm64 test vet test-race dev dev-infisical prod prod-infisical sql generate docker docker-distroless swagger swagger-fmt # Deps deps: @@ -105,4 +105,8 @@ docker-distroless: # Swagger swagger: - swag init -d ./internal/bootstrap -g router_bootstrap.go -o ./internal/swagger \ No newline at end of file + swag init -d ./internal -g bootstrap/router_bootstrap.go -o ./internal/swagger + +# Swagger Format +swagger-fmt: + swag fmt -d ./internal -g bootstrap/router_bootstrap.go \ No newline at end of file diff --git a/internal/bootstrap/router_bootstrap.go b/internal/bootstrap/router_bootstrap.go index 26d55e13..654ad367 100644 --- a/internal/bootstrap/router_bootstrap.go +++ b/internal/bootstrap/router_bootstrap.go @@ -22,12 +22,12 @@ import ( "github.com/gin-gonic/gin" ) -// @title Tinyauth API -// @version development -// @description Swagger documentation for Tinyauth's API. -// @license.name AGPL-3.0 -// @license.url https://github.com/tinyauthapp/tinyauth/blob/main/LICENSE -// @BasePath /api +// @title Tinyauth API +// @version development +// @description Swagger documentation for Tinyauth's API. +// @license.name AGPL-3.0 +// @license.url https://github.com/tinyauthapp/tinyauth/blob/main/LICENSE +// @BasePath / func (app *BootstrapApp) setupRouter() error { // we don't want gin debug mode gin.SetMode(gin.ReleaseMode) diff --git a/internal/controller/context_controller.go b/internal/controller/context_controller.go index abfabaad..44a26df3 100644 --- a/internal/controller/context_controller.go +++ b/internal/controller/context_controller.go @@ -107,6 +107,14 @@ func NewContextController(i ContextControllerInput) *ContextController { return controller } +// UserContext godoc +// +// @Summary User context +// @Description Get the user context +// @Tags context +// @Produce json +// @Success 200 {object} UserContextResponse +// @Router /api/context/user [get] func (controller *ContextController) userContextHandler(c *gin.Context) { context, err := new(model.UserContext).NewFromGin(c) @@ -147,6 +155,14 @@ func (controller *ContextController) userContextHandler(c *gin.Context) { c.JSON(200, userContext) } +// AppContext godoc +// +// @Summary App context +// @Description Get the app context +// @Tags context +// @Produce json +// @Success 200 {object} AppContextResponse +// @Router /api/context/app [get] func (controller *ContextController) appContextHandler(c *gin.Context) { c.JSON(200, AppContextResponse{ Status: 200, diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 8b6ab4f7..54767818 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -7,6 +7,10 @@ const ( FrontendLoginForApp FrontendLoginFor = "app" ) +type SimpleResponse struct { + Status int `json:"status"` + Message string `json:"message,omitempty"` +} type UnauthorizedQuery struct { Username string `url:"username"` Resource string `url:"resource"` diff --git a/internal/controller/health_controller.go b/internal/controller/health_controller.go index 2b578978..9f50a23c 100644 --- a/internal/controller/health_controller.go +++ b/internal/controller/health_controller.go @@ -23,9 +23,18 @@ func NewHealthController(i HealthControllerInput) *HealthController { return controller } +// HealthCheck godoc +// +// @Summary Healthcheck +// @Description Check if the server is up and running +// @Tags health +// @Produce json +// @Success 200 {object} SimpleResponse +// @Router /api/healthz [get] +// @Router /api/healthz [head] func (controller *HealthController) healthHandler(c *gin.Context) { - c.JSON(200, gin.H{ - "status": 200, - "message": "Healthy", + c.JSON(200, SimpleResponse{ + Status: 200, + Message: "OK", }) } diff --git a/internal/controller/oauth_controller.go b/internal/controller/oauth_controller.go index 27fca206..d0f7e52d 100644 --- a/internal/controller/oauth_controller.go +++ b/internal/controller/oauth_controller.go @@ -54,6 +54,27 @@ func NewOAuthController(i OAuthControllerInput) *OAuthController { return controller } +type OAuthURLSuccessResponse struct { + SimpleResponse + URL string `json:"url"` +} + +// OAuthURL godoc +// +// @Summary OAuth URL +// @Description Get an OAuth URL for the specified provider +// @Tags oauth +// @Produce json +// @Param id path string true "Provider ID" +// @Param login_for query string false "Login for" +// @Param oidc_ticket query string false "OpenID Connect Ticket" +// @Param oidc_scope query string false "OpenID Connect Scope" +// @Param oidc_name query string false "OpenID Connect Name" +// @Param redirect_uri query string false "Redirect URI" +// @Success 200 {object} OAuthURLSuccessResponse +// @Failure 400 {object} SimpleResponse +// @Failure 500 {object} SimpleResponse +// @Router /api/oauth/url/{id} [get] func (controller *OAuthController) oauthURLHandler(c *gin.Context) { var req OAuthRequest @@ -111,23 +132,33 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) { c.SetCookie(controller.runtime.OAuthSessionCookieName, sessionId, int(time.Hour.Seconds()), "/", controller.getCookieDomain(), controller.config.Auth.SecureCookie, true) - c.JSON(200, gin.H{ - "status": 200, - "message": "OK", - "url": authUrl, + c.JSON(200, OAuthURLSuccessResponse{ + SimpleResponse: SimpleResponse{ + Status: 200, + Message: "OK", + }, + URL: authUrl, }) } +// OAuthCallback godoc +// +// @Summary OAuth Callback +// @Description Callback URL for OAuth providers +// @Tags oauth +// @Param id path string true "Provider ID" +// @Param code query string true "State" +// @Param state query string true "Code" +// @Success 302 +// @Failure 302 +// @Router /api/oauth/callback/{id} [get] func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { var req OAuthRequest err := c.BindUri(&req) if err != nil { - controller.log.App.Error().Err(err).Msg("Failed to bind URI") - c.JSON(400, gin.H{ - "status": 400, - "message": "Bad Request", - }) + controller.log.App.Error().Err(err).Msg("Failed to get provider ID") + c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL)) return } @@ -135,7 +166,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { if err != nil { controller.log.App.Error().Err(err).Msg("Failed to get OAuth session cookie") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL)) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL)) return } @@ -145,7 +176,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { if err != nil { controller.log.App.Error().Err(err).Msg("Failed to get pending OAuth session") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL)) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL)) return } @@ -154,7 +185,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { state := c.Query("state") if state != oauthPendingSession.State { controller.log.App.Warn().Msg("OAuth state mismatch") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL)) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL)) return } @@ -163,7 +194,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { if err != nil { controller.log.App.Error().Err(err).Msg("Failed to exchange code for token") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL)) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL)) return } @@ -171,19 +202,19 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { if err != nil { controller.log.App.Error().Err(err).Msg("Failed to get user info from OAuth provider") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL)) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL)) return } if user == nil { controller.log.App.Warn().Msg("OAuth provider did not return user info") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL)) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL)) return } if user.Email == "" { controller.log.App.Warn().Msg("OAuth provider did not return an email") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL)) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL)) return } @@ -191,13 +222,13 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { if err != nil { controller.log.App.Error().Err(err).Msg("Failed to get OAuth service for session") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL)) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL)) return } if svc.ID() != req.Provider { controller.log.App.Warn().Msgf("OAuth provider mismatch: expected %s, got %s", req.Provider, svc.ID()) - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL)) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL)) return } @@ -211,11 +242,11 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { if err != nil { controller.log.App.Error().Err(err).Msg("Failed to encode unauthorized query") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL)) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL)) return } - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", controller.runtime.AppURL, queries.Encode())) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/unauthorized?%s", controller.runtime.AppURL, queries.Encode())) return } @@ -260,7 +291,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { if err != nil { controller.log.App.Error().Err(err).Msg("Failed to create session cookie") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL)) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL)) return } @@ -273,10 +304,10 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { queries, err := query.Values(oauthPendingSession.CallbackParams) if err != nil { controller.log.App.Error().Err(err).Msg("Failed to encode OIDC callback query") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL)) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL)) return } - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/oidc/authorize?%s", controller.runtime.AppURL, queries.Encode())) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/oidc/authorize?%s", controller.runtime.AppURL, queries.Encode())) return } @@ -288,15 +319,15 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { if err != nil { controller.log.App.Error().Err(err).Msg("Failed to encode redirect query") - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.runtime.AppURL)) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/error", controller.runtime.AppURL)) return } - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/continue?%s", controller.runtime.AppURL, queries.Encode())) + c.Redirect(http.StatusFound, fmt.Sprintf("%s/continue?%s", controller.runtime.AppURL, queries.Encode())) return } - c.Redirect(http.StatusTemporaryRedirect, controller.runtime.AppURL) + c.Redirect(http.StatusFound, controller.runtime.AppURL) } func (controller *OAuthController) isOidcRequest(params service.OAuthCallbackParams) bool { diff --git a/internal/controller/oidc_controller.go b/internal/controller/oidc_controller.go index c4049953..edbdc637 100644 --- a/internal/controller/oidc_controller.go +++ b/internal/controller/oidc_controller.go @@ -82,6 +82,15 @@ type AuthorizeCompleteRequest struct { Ticket string `json:"ticket" binding:"required"` } +type AuthorizeCompleteResponse struct { + SimpleResponse + RedirectURI string `json:"redirect_uri"` +} + +type OIDCErrorResponse struct { + Error string `json:"error"` +} + type OIDCControllerInput struct { dig.In @@ -114,6 +123,36 @@ func NewOIDCController(i OIDCControllerInput) *OIDCController { // This endpoint does **not** return a code, it handles param validation, ticket creation // and then redirects to the frontend to handle the consent screen. It performs no destructive // actions (like logging out an existing session) +// Authorize godoc +// +// @Summary Authorize +// @Description OpenID Connect Authorize Endpoint +// @Accept x-www-form-urlencoded +// @Tags oidc +// @Param scope query string false "OAuth scopes (space separated, must include openid)" +// @Param response_type query string false "Response type (e.g. code)" +// @Param client_id query string false "Client ID" +// @Param redirect_uri query string false "Redirect URI" +// @Param state query string false "Opaque state value returned to the client" +// @Param nonce query string false "Nonce for ID token replay protection" +// @Param code_challenge query string false "PKCE code challenge" +// @Param code_challenge_method query string false "PKCE code challenge method (S256 or plain)" +// @Param prompt query string false "Prompt parameter (none, login, consent)" +// @Param max_age query string false "Max authentication age in seconds" +// @Param scope formData string false "OAuth scopes (space separated, must include openid)" +// @Param response_type formData string false "Response type (e.g. code)" +// @Param client_id formData string false "Client ID" +// @Param redirect_uri formData string false "Redirect URI" +// @Param state formData string false "Opaque state value returned to the client" +// @Param nonce formData string false "Nonce for ID token replay protection" +// @Param code_challenge formData string false "PKCE code challenge" +// @Param code_challenge_method formData string false "PKCE code challenge method (S256 or plain)" +// @Param prompt formData string false "Prompt parameter (none, login, consent)" +// @Param max_age formData string false "Max authentication age in seconds" +// @Success 302 +// @Failure 302 +// @Router /authorize [get] +// @Router /authorize [post] func (controller *OIDCController) authorize(c *gin.Context) { if controller.oidc == nil { controller.authorizeError(c, authorizeErrorParams{ @@ -261,6 +300,16 @@ func (controller *OIDCController) authorize(c *gin.Context) { // The actual **internal** endpoint that actually creates the code and session. // It is called by the frontend after the user has logged in and given consent. +// AuthorizeComplete godoc +// +// @Summary Authorize Complete +// @Description Internal endpoint for the completion of the OpenID Connect authorization flow +// @Tags oidc +// @Accept json +// @Produce json +// @Success 200 {object} AuthorizeCompleteResponse +// @Failure 500 +// @Router /api/oidc/authorize-complete [post] func (controller *OIDCController) authorizeComplete(c *gin.Context) { if controller.oidc == nil { // For this endpoint we return JSON errors since it's called @@ -361,17 +410,44 @@ func (controller *OIDCController) authorizeComplete(c *gin.Context) { return } - c.JSON(200, gin.H{ - "status": 200, - "redirect_uri": fmt.Sprintf("%s?%s", authorizeReq.RedirectURI, queries.Encode()), + c.JSON(200, AuthorizeCompleteResponse{ + SimpleResponse: SimpleResponse{ + Status: 200, + }, + RedirectURI: fmt.Sprintf("%s?%s", authorizeReq.RedirectURI, queries.Encode()), }) } +// Token godoc +// +// @Summary Token +// @Description OpenID Connect Token Endpoint +// @Tags oidc +// @Accept x-www-form-urlencoded +// @Produce json +// @Param grant_type query string true "Grant type (authorization_code or refresh_token)" +// @Param code query string false "Authorization code (required for authorization_code grant)" +// @Param redirect_uri query string false "Redirect URI (must match the one from the authorize request)" +// @Param refresh_token query string false "Refresh token (required for refresh_token grant)" +// @Param client_id query string false "Client ID (required if not using Basic auth)" +// @Param client_secret query string false "Client secret (required for confidential clients without Basic auth)" +// @Param code_verifier query string false "PKCE code verifier (required if code_challenge was sent)" +// @Param grant_type formData string false "Grant type (authorization_code or refresh_token)" +// @Param code formData string false "Authorization code (required for authorization_code grant)" +// @Param redirect_uri formData string false "Redirect URI (must match the one from the authorize request)" +// @Param refresh_token formData string false "Refresh token (required for refresh_token grant)" +// @Param client_id formData string false "Client ID (required if not using Basic auth)" +// @Param client_secret formData string false "Client secret (required for confidential clients without Basic auth)" +// @Param code_verifier formData string false "PKCE code verifier (required if code_challenge was sent)" +// @Success 200 {object} service.TokenResponse +// @Failure 400 {object} OIDCErrorResponse +// @Failure 500 {object} OIDCErrorResponse +// @Router /oidc/token [post] func (controller *OIDCController) Token(c *gin.Context) { if controller.oidc == nil { controller.log.App.Warn().Msg("Received OIDC request but OIDC server is not configured") - c.JSON(500, gin.H{ - "error": "server_error", + c.JSON(500, OIDCErrorResponse{ + Error: "server_error", }) return } @@ -381,8 +457,8 @@ func (controller *OIDCController) Token(c *gin.Context) { err := c.Bind(&req) if err != nil { controller.log.App.Warn().Err(err).Msg("Failed to bind token request") - c.JSON(400, gin.H{ - "error": "invalid_request", + c.JSON(400, OIDCErrorResponse{ + Error: "invalid_request", }) return } @@ -390,8 +466,8 @@ func (controller *OIDCController) Token(c *gin.Context) { err = controller.oidc.ValidateGrantType(req.GrantType) if err != nil { controller.log.App.Warn().Err(err).Msg("Invalid grant type") - c.JSON(400, gin.H{ - "error": err.Error(), + c.JSON(400, OIDCErrorResponse{ + Error: err.Error(), }) return } @@ -411,8 +487,8 @@ func (controller *OIDCController) Token(c *gin.Context) { if !ok { controller.log.App.Warn().Msg("Client credentials not found in basic auth") c.Header("www-authenticate", `Basic realm="Tinyauth OIDC Token Endpoint"`) - c.JSON(400, gin.H{ - "error": "invalid_client", + c.JSON(400, OIDCErrorResponse{ + Error: "invalid_client", }) return } @@ -427,16 +503,16 @@ func (controller *OIDCController) Token(c *gin.Context) { if !ok { controller.log.App.Warn().Str("clientId", creds.ClientID).Msg("Client not found") - c.JSON(400, gin.H{ - "error": "invalid_client", + c.JSON(400, OIDCErrorResponse{ + Error: "invalid_client", }) return } if client.ClientSecret != creds.ClientSecret { controller.log.App.Warn().Str("clientId", creds.ClientID).Msg("Invalid client secret") - c.JSON(400, gin.H{ - "error": "invalid_client", + c.JSON(400, OIDCErrorResponse{ + Error: "invalid_client", }) return } @@ -457,15 +533,15 @@ func (controller *OIDCController) Token(c *gin.Context) { if err != nil { controller.log.App.Error().Err(err).Msg("Failed to delete session for reused code") } - c.JSON(400, gin.H{ - "error": "invalid_grant", + c.JSON(400, OIDCErrorResponse{ + Error: "invalid_grant", }) return } controller.log.App.Warn().Msg("Code not found") - c.JSON(400, gin.H{ - "error": "invalid_grant", + c.JSON(400, OIDCErrorResponse{ + Error: "invalid_grant", }) return } @@ -475,8 +551,8 @@ func (controller *OIDCController) Token(c *gin.Context) { if entry.RedirectURI != req.RedirectURI { controller.log.App.Warn().Msg("Redirect URI does not match") - c.JSON(400, gin.H{ - "error": "invalid_grant", + c.JSON(400, OIDCErrorResponse{ + Error: "invalid_grant", }) return } @@ -485,8 +561,8 @@ func (controller *OIDCController) Token(c *gin.Context) { if !ok { controller.log.App.Warn().Msg("PKCE validation failed") - c.JSON(400, gin.H{ - "error": "invalid_grant", + c.JSON(400, OIDCErrorResponse{ + Error: "invalid_grant", }) return } @@ -495,8 +571,8 @@ func (controller *OIDCController) Token(c *gin.Context) { if err != nil { controller.log.App.Error().Err(err).Msg("Failed to generate access token") - c.JSON(400, gin.H{ - "error": "server_error", + c.JSON(400, OIDCErrorResponse{ + Error: "server_error", }) return } @@ -508,23 +584,23 @@ func (controller *OIDCController) Token(c *gin.Context) { if err != nil { if errors.Is(err, service.ErrTokenExpired) { controller.log.App.Warn().Msg("Refresh token expired") - c.JSON(400, gin.H{ - "error": "invalid_grant", + c.JSON(400, OIDCErrorResponse{ + Error: "invalid_grant", }) return } if errors.Is(err, service.ErrInvalidClient) { controller.log.App.Warn().Msg("Refresh token does not belong to client") - c.JSON(400, gin.H{ - "error": "invalid_grant", + c.JSON(400, OIDCErrorResponse{ + Error: "invalid_grant", }) return } controller.log.App.Error().Err(err).Msg("Failed to refresh access token") - c.JSON(400, gin.H{ - "error": "server_error", + c.JSON(400, OIDCErrorResponse{ + Error: "server_error", }) return } @@ -538,11 +614,25 @@ func (controller *OIDCController) Token(c *gin.Context) { c.JSON(200, tokenResponse) } +// Userinfo godoc +// +// @Summary Userinfo +// @Description OpenID Connect Userinfo Endpoint +// @Accept x-www-form-urlencoded +// @Tags oidc +// @Param access_token formData string false "OpenID Connect Access Token" +// @Produce json +// @Success 200 {object} service.UserinfoResponse +// @Failure 400 {object} OIDCErrorResponse +// @Failure 401 {object} OIDCErrorResponse +// @Failure 500 {object} OIDCErrorResponse +// @Router /oidc/userinfo [get] +// @Router /oidc/userinfo [post] func (controller *OIDCController) Userinfo(c *gin.Context) { if controller.oidc == nil { controller.log.App.Warn().Msg("Received OIDC userinfo request but OIDC server is not configured") - c.JSON(500, gin.H{ - "error": "server_error", + c.JSON(500, OIDCErrorResponse{ + Error: "server_error", }) return } @@ -554,16 +644,16 @@ func (controller *OIDCController) Userinfo(c *gin.Context) { tokenType, bearerToken, ok := strings.Cut(authorization, " ") if !ok { controller.log.App.Warn().Msg("OIDC userinfo accessed with invalid authorization header") - c.JSON(401, gin.H{ - "error": "invalid_request", + c.JSON(401, OIDCErrorResponse{ + Error: "invalid_request", }) return } if strings.ToLower(tokenType) != "bearer" { controller.log.App.Warn().Msg("OIDC userinfo accessed with non-bearer token") - c.JSON(401, gin.H{ - "error": "invalid_request", + c.JSON(401, OIDCErrorResponse{ + Error: "invalid_request", }) return } @@ -572,23 +662,23 @@ func (controller *OIDCController) Userinfo(c *gin.Context) { } else if c.Request.Method == http.MethodPost { if c.ContentType() != "application/x-www-form-urlencoded" { controller.log.App.Warn().Msg("OIDC userinfo POST accessed with invalid content type") - c.JSON(400, gin.H{ - "error": "invalid_request", + c.JSON(400, OIDCErrorResponse{ + Error: "invalid_request", }) return } token = c.PostForm("access_token") if token == "" { controller.log.App.Warn().Msg("OIDC userinfo POST accessed without access_token") - c.JSON(401, gin.H{ - "error": "invalid_request", + c.JSON(401, OIDCErrorResponse{ + Error: "invalid_request", }) return } } else { controller.log.App.Warn().Msg("OIDC userinfo accessed without authorization header or POST body") - c.JSON(401, gin.H{ - "error": "invalid_request", + c.JSON(401, OIDCErrorResponse{ + Error: "invalid_request", }) return } @@ -598,15 +688,15 @@ func (controller *OIDCController) Userinfo(c *gin.Context) { if err != nil { if errors.Is(err, service.ErrTokenNotFound) { controller.log.App.Warn().Msg("OIDC userinfo accessed with invalid token") - c.JSON(401, gin.H{ - "error": "invalid_grant", + c.JSON(401, OIDCErrorResponse{ + Error: "invalid_grant", }) return } controller.log.App.Error().Err(err).Msg("Failed to get access token") - c.JSON(401, gin.H{ - "error": "server_error", + c.JSON(401, OIDCErrorResponse{ + Error: "server_error", }) return } @@ -614,8 +704,8 @@ func (controller *OIDCController) Userinfo(c *gin.Context) { // If we don't have the openid scope, return an error if !slices.Contains(strings.Split(entry.Scope, " "), "openid") { controller.log.App.Warn().Msg("OIDC userinfo accessed with missing openid scope") - c.JSON(401, gin.H{ - "error": "invalid_scope", + c.JSON(401, OIDCErrorResponse{ + Error: "invalid_scope", }) return } @@ -626,8 +716,8 @@ func (controller *OIDCController) Userinfo(c *gin.Context) { if err != nil { controller.log.App.Error().Err(err).Msg("Failed to get user info") - c.JSON(401, gin.H{ - "error": "server_error", + c.JSON(401, OIDCErrorResponse{ + Error: "server_error", }) return } @@ -662,9 +752,11 @@ func (controller *OIDCController) authorizeError(c *gin.Context, params authoriz redirectUrl := fmt.Sprintf("%s?%s", params.callback, queries.Encode()) if params.json { - c.JSON(200, gin.H{ - "status": 200, - "redirect_uri": redirectUrl, + c.JSON(200, AuthorizeCompleteResponse{ + SimpleResponse: SimpleResponse{ + Status: 200, + }, + RedirectURI: redirectUrl, }) return } @@ -694,9 +786,11 @@ func (controller *OIDCController) authorizeError(c *gin.Context, params authoriz } if params.json { - c.JSON(200, gin.H{ - "status": 200, - "redirect_uri": redirectUrl, + c.JSON(200, AuthorizeCompleteResponse{ + SimpleResponse: SimpleResponse{ + Status: 200, + }, + RedirectURI: redirectUrl, }) return } diff --git a/internal/swagger/docs.go b/internal/swagger/docs.go index 3be445b4..30ac535b 100644 --- a/internal/swagger/docs.go +++ b/internal/swagger/docs.go @@ -18,14 +18,1044 @@ const docTemplate = `{ }, "host": "{{.Host}}", "basePath": "{{.BasePath}}", - "paths": {} + "paths": { + "/api/context/app": { + "get": { + "description": "Get the app context", + "produces": [ + "application/json" + ], + "tags": [ + "context" + ], + "summary": "App context", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.AppContextResponse" + } + } + } + } + }, + "/api/context/user": { + "get": { + "description": "Get the user context", + "produces": [ + "application/json" + ], + "tags": [ + "context" + ], + "summary": "User context", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.UserContextResponse" + } + } + } + } + }, + "/api/healthz": { + "get": { + "description": "Check if the server is up and running", + "produces": [ + "application/json" + ], + "tags": [ + "health" + ], + "summary": "Healthcheck", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + }, + "head": { + "description": "Check if the server is up and running", + "produces": [ + "application/json" + ], + "tags": [ + "health" + ], + "summary": "Healthcheck", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/api/oauth/callback/{id}": { + "get": { + "description": "Callback URL for OAuth providers", + "tags": [ + "oauth" + ], + "summary": "OAuth Callback", + "parameters": [ + { + "type": "string", + "description": "Provider ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "State", + "name": "code", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Code", + "name": "state", + "in": "query", + "required": true + } + ], + "responses": { + "302": { + "description": "Found" + } + } + } + }, + "/api/oauth/url/{id}": { + "get": { + "description": "Get an OAuth URL for the specified provider", + "produces": [ + "application/json" + ], + "tags": [ + "oauth" + ], + "summary": "OAuth URL", + "parameters": [ + { + "type": "string", + "description": "Provider ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Login for", + "name": "login_for", + "in": "query" + }, + { + "type": "string", + "description": "OpenID Connect Ticket", + "name": "oidc_ticket", + "in": "query" + }, + { + "type": "string", + "description": "OpenID Connect Scope", + "name": "oidc_scope", + "in": "query" + }, + { + "type": "string", + "description": "OpenID Connect Name", + "name": "oidc_name", + "in": "query" + }, + { + "type": "string", + "description": "Redirect URI", + "name": "redirect_uri", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.OAuthURLSuccessResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/api/oidc/authorize-complete": { + "post": { + "description": "Internal endpoint for the completion of the OpenID Connect authorization flow", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "oidc" + ], + "summary": "Authorize Complete", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.AuthorizeCompleteResponse" + } + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, + "/authorize": { + "get": { + "description": "OpenID Connect Authorize Endpoint", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "tags": [ + "oidc" + ], + "summary": "Authorize", + "parameters": [ + { + "type": "string", + "description": "OAuth scopes (space separated, must include openid)", + "name": "scope", + "in": "query" + }, + { + "type": "string", + "description": "Response type (e.g. code)", + "name": "response_type", + "in": "query" + }, + { + "type": "string", + "description": "Client ID", + "name": "client_id", + "in": "query" + }, + { + "type": "string", + "description": "Redirect URI", + "name": "redirect_uri", + "in": "query" + }, + { + "type": "string", + "description": "Opaque state value returned to the client", + "name": "state", + "in": "query" + }, + { + "type": "string", + "description": "Nonce for ID token replay protection", + "name": "nonce", + "in": "query" + }, + { + "type": "string", + "description": "PKCE code challenge", + "name": "code_challenge", + "in": "query" + }, + { + "type": "string", + "description": "PKCE code challenge method (S256 or plain)", + "name": "code_challenge_method", + "in": "query" + }, + { + "type": "string", + "description": "Prompt parameter (none, login, consent)", + "name": "prompt", + "in": "query" + }, + { + "type": "string", + "description": "Max authentication age in seconds", + "name": "max_age", + "in": "query" + }, + { + "type": "string", + "description": "OAuth scopes (space separated, must include openid)", + "name": "scope", + "in": "formData" + }, + { + "type": "string", + "description": "Response type (e.g. code)", + "name": "response_type", + "in": "formData" + }, + { + "type": "string", + "description": "Client ID", + "name": "client_id", + "in": "formData" + }, + { + "type": "string", + "description": "Redirect URI", + "name": "redirect_uri", + "in": "formData" + }, + { + "type": "string", + "description": "Opaque state value returned to the client", + "name": "state", + "in": "formData" + }, + { + "type": "string", + "description": "Nonce for ID token replay protection", + "name": "nonce", + "in": "formData" + }, + { + "type": "string", + "description": "PKCE code challenge", + "name": "code_challenge", + "in": "formData" + }, + { + "type": "string", + "description": "PKCE code challenge method (S256 or plain)", + "name": "code_challenge_method", + "in": "formData" + }, + { + "type": "string", + "description": "Prompt parameter (none, login, consent)", + "name": "prompt", + "in": "formData" + }, + { + "type": "string", + "description": "Max authentication age in seconds", + "name": "max_age", + "in": "formData" + } + ], + "responses": { + "302": { + "description": "Found" + } + } + }, + "post": { + "description": "OpenID Connect Authorize Endpoint", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "tags": [ + "oidc" + ], + "summary": "Authorize", + "parameters": [ + { + "type": "string", + "description": "OAuth scopes (space separated, must include openid)", + "name": "scope", + "in": "query" + }, + { + "type": "string", + "description": "Response type (e.g. code)", + "name": "response_type", + "in": "query" + }, + { + "type": "string", + "description": "Client ID", + "name": "client_id", + "in": "query" + }, + { + "type": "string", + "description": "Redirect URI", + "name": "redirect_uri", + "in": "query" + }, + { + "type": "string", + "description": "Opaque state value returned to the client", + "name": "state", + "in": "query" + }, + { + "type": "string", + "description": "Nonce for ID token replay protection", + "name": "nonce", + "in": "query" + }, + { + "type": "string", + "description": "PKCE code challenge", + "name": "code_challenge", + "in": "query" + }, + { + "type": "string", + "description": "PKCE code challenge method (S256 or plain)", + "name": "code_challenge_method", + "in": "query" + }, + { + "type": "string", + "description": "Prompt parameter (none, login, consent)", + "name": "prompt", + "in": "query" + }, + { + "type": "string", + "description": "Max authentication age in seconds", + "name": "max_age", + "in": "query" + }, + { + "type": "string", + "description": "OAuth scopes (space separated, must include openid)", + "name": "scope", + "in": "formData" + }, + { + "type": "string", + "description": "Response type (e.g. code)", + "name": "response_type", + "in": "formData" + }, + { + "type": "string", + "description": "Client ID", + "name": "client_id", + "in": "formData" + }, + { + "type": "string", + "description": "Redirect URI", + "name": "redirect_uri", + "in": "formData" + }, + { + "type": "string", + "description": "Opaque state value returned to the client", + "name": "state", + "in": "formData" + }, + { + "type": "string", + "description": "Nonce for ID token replay protection", + "name": "nonce", + "in": "formData" + }, + { + "type": "string", + "description": "PKCE code challenge", + "name": "code_challenge", + "in": "formData" + }, + { + "type": "string", + "description": "PKCE code challenge method (S256 or plain)", + "name": "code_challenge_method", + "in": "formData" + }, + { + "type": "string", + "description": "Prompt parameter (none, login, consent)", + "name": "prompt", + "in": "formData" + }, + { + "type": "string", + "description": "Max authentication age in seconds", + "name": "max_age", + "in": "formData" + } + ], + "responses": { + "302": { + "description": "Found" + } + } + } + }, + "/oidc/token": { + "post": { + "description": "OpenID Connect Token Endpoint", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "tags": [ + "oidc" + ], + "summary": "Token", + "parameters": [ + { + "type": "string", + "description": "Grant type (authorization_code or refresh_token)", + "name": "grant_type", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Authorization code (required for authorization_code grant)", + "name": "code", + "in": "query" + }, + { + "type": "string", + "description": "Redirect URI (must match the one from the authorize request)", + "name": "redirect_uri", + "in": "query" + }, + { + "type": "string", + "description": "Refresh token (required for refresh_token grant)", + "name": "refresh_token", + "in": "query" + }, + { + "type": "string", + "description": "Client ID (required if not using Basic auth)", + "name": "client_id", + "in": "query" + }, + { + "type": "string", + "description": "Client secret (required for confidential clients without Basic auth)", + "name": "client_secret", + "in": "query" + }, + { + "type": "string", + "description": "PKCE code verifier (required if code_challenge was sent)", + "name": "code_verifier", + "in": "query" + }, + { + "type": "string", + "description": "Grant type (authorization_code or refresh_token)", + "name": "grant_type", + "in": "formData" + }, + { + "type": "string", + "description": "Authorization code (required for authorization_code grant)", + "name": "code", + "in": "formData" + }, + { + "type": "string", + "description": "Redirect URI (must match the one from the authorize request)", + "name": "redirect_uri", + "in": "formData" + }, + { + "type": "string", + "description": "Refresh token (required for refresh_token grant)", + "name": "refresh_token", + "in": "formData" + }, + { + "type": "string", + "description": "Client ID (required if not using Basic auth)", + "name": "client_id", + "in": "formData" + }, + { + "type": "string", + "description": "Client secret (required for confidential clients without Basic auth)", + "name": "client_secret", + "in": "formData" + }, + { + "type": "string", + "description": "PKCE code verifier (required if code_challenge was sent)", + "name": "code_verifier", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service.TokenResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + } + } + } + }, + "/oidc/userinfo": { + "get": { + "description": "OpenID Connect Userinfo Endpoint", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "tags": [ + "oidc" + ], + "summary": "Userinfo", + "parameters": [ + { + "type": "string", + "description": "OpenID Connect Access Token", + "name": "access_token", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service.UserinfoResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + } + } + }, + "post": { + "description": "OpenID Connect Userinfo Endpoint", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "tags": [ + "oidc" + ], + "summary": "Userinfo", + "parameters": [ + { + "type": "string", + "description": "OpenID Connect Access Token", + "name": "access_token", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service.UserinfoResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + } + } + } + } + }, + "definitions": { + "controller.ACRApp": { + "type": "object", + "properties": { + "appUrl": { + "type": "string" + }, + "cookieDomain": { + "type": "string" + }, + "subdomainsEnabled": { + "type": "boolean" + } + } + }, + "controller.ACRAuth": { + "type": "object", + "properties": { + "providers": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Provider" + } + } + } + }, + "controller.ACROAuth": { + "type": "object", + "properties": { + "autoRedirect": { + "type": "string" + } + } + }, + "controller.ACRUI": { + "type": "object", + "properties": { + "backgroundImage": { + "type": "string" + }, + "forgotPasswordMessage": { + "type": "string" + }, + "title": { + "type": "string" + }, + "warningsEnabled": { + "type": "boolean" + } + } + }, + "controller.AppContextResponse": { + "type": "object", + "properties": { + "app": { + "$ref": "#/definitions/controller.ACRApp" + }, + "auth": { + "$ref": "#/definitions/controller.ACRAuth" + }, + "message": { + "type": "string" + }, + "oauth": { + "$ref": "#/definitions/controller.ACROAuth" + }, + "status": { + "type": "integer" + }, + "ui": { + "$ref": "#/definitions/controller.ACRUI" + } + } + }, + "controller.AuthorizeCompleteResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "redirect_uri": { + "type": "string" + }, + "status": { + "type": "integer" + } + } + }, + "controller.OAuthURLSuccessResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "url": { + "type": "string" + } + } + }, + "controller.OIDCErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "controller.SimpleResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + } + }, + "controller.UCRAuth": { + "type": "object", + "properties": { + "authenticated": { + "type": "boolean" + }, + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "providerId": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "controller.UCROAuth": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "displayName": { + "type": "string" + } + } + }, + "controller.UCRTOTP": { + "type": "object", + "properties": { + "pending": { + "type": "boolean" + } + } + }, + "controller.UCRTailscale": { + "type": "object", + "properties": { + "nodeName": { + "type": "string" + } + } + }, + "controller.UserContextResponse": { + "type": "object", + "properties": { + "auth": { + "$ref": "#/definitions/controller.UCRAuth" + }, + "message": { + "type": "string" + }, + "oauth": { + "$ref": "#/definitions/controller.UCROAuth" + }, + "status": { + "type": "integer" + }, + "tailscale": { + "$ref": "#/definitions/controller.UCRTailscale" + }, + "totp": { + "$ref": "#/definitions/controller.UCRTOTP" + } + } + }, + "model.AddressClaim": { + "type": "object", + "properties": { + "country": { + "type": "string" + }, + "formatted": { + "type": "string" + }, + "locality": { + "type": "string" + }, + "postal_code": { + "type": "string" + }, + "region": { + "type": "string" + }, + "street_address": { + "type": "string" + } + } + }, + "model.Provider": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "oauth": { + "type": "boolean" + } + } + }, + "service.TokenResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "expires_in": { + "type": "integer" + }, + "id_token": { + "type": "string" + }, + "refresh_token": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "token_type": { + "type": "string" + } + } + }, + "service.UserinfoResponse": { + "type": "object", + "properties": { + "address": { + "$ref": "#/definitions/model.AddressClaim" + }, + "birthdate": { + "type": "string" + }, + "email": { + "type": "string" + }, + "email_verified": { + "type": "boolean" + }, + "family_name": { + "type": "string" + }, + "gender": { + "type": "string" + }, + "given_name": { + "type": "string" + }, + "groups": { + "type": "array", + "items": { + "type": "string" + } + }, + "locale": { + "type": "string" + }, + "middle_name": { + "type": "string" + }, + "name": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "phone_number": { + "type": "string" + }, + "phone_number_verified": { + "type": "boolean" + }, + "picture": { + "type": "string" + }, + "preferred_username": { + "type": "string" + }, + "profile": { + "type": "string" + }, + "sub": { + "type": "string" + }, + "updated_at": { + "type": "integer" + }, + "website": { + "type": "string" + }, + "zoneinfo": { + "type": "string" + } + } + } + } }` // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ Version: "development", Host: "", - BasePath: "/api", + BasePath: "/", Schemes: []string{}, Title: "Tinyauth API", Description: "Swagger documentation for Tinyauth's API.", diff --git a/internal/swagger/swagger.json b/internal/swagger/swagger.json index 25db347d..de6cceb8 100644 --- a/internal/swagger/swagger.json +++ b/internal/swagger/swagger.json @@ -10,6 +10,1036 @@ }, "version": "development" }, - "basePath": "/api", - "paths": {} + "basePath": "/", + "paths": { + "/api/context/app": { + "get": { + "description": "Get the app context", + "produces": [ + "application/json" + ], + "tags": [ + "context" + ], + "summary": "App context", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.AppContextResponse" + } + } + } + } + }, + "/api/context/user": { + "get": { + "description": "Get the user context", + "produces": [ + "application/json" + ], + "tags": [ + "context" + ], + "summary": "User context", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.UserContextResponse" + } + } + } + } + }, + "/api/healthz": { + "get": { + "description": "Check if the server is up and running", + "produces": [ + "application/json" + ], + "tags": [ + "health" + ], + "summary": "Healthcheck", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + }, + "head": { + "description": "Check if the server is up and running", + "produces": [ + "application/json" + ], + "tags": [ + "health" + ], + "summary": "Healthcheck", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/api/oauth/callback/{id}": { + "get": { + "description": "Callback URL for OAuth providers", + "tags": [ + "oauth" + ], + "summary": "OAuth Callback", + "parameters": [ + { + "type": "string", + "description": "Provider ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "State", + "name": "code", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Code", + "name": "state", + "in": "query", + "required": true + } + ], + "responses": { + "302": { + "description": "Found" + } + } + } + }, + "/api/oauth/url/{id}": { + "get": { + "description": "Get an OAuth URL for the specified provider", + "produces": [ + "application/json" + ], + "tags": [ + "oauth" + ], + "summary": "OAuth URL", + "parameters": [ + { + "type": "string", + "description": "Provider ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Login for", + "name": "login_for", + "in": "query" + }, + { + "type": "string", + "description": "OpenID Connect Ticket", + "name": "oidc_ticket", + "in": "query" + }, + { + "type": "string", + "description": "OpenID Connect Scope", + "name": "oidc_scope", + "in": "query" + }, + { + "type": "string", + "description": "OpenID Connect Name", + "name": "oidc_name", + "in": "query" + }, + { + "type": "string", + "description": "Redirect URI", + "name": "redirect_uri", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.OAuthURLSuccessResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.SimpleResponse" + } + } + } + } + }, + "/api/oidc/authorize-complete": { + "post": { + "description": "Internal endpoint for the completion of the OpenID Connect authorization flow", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "oidc" + ], + "summary": "Authorize Complete", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controller.AuthorizeCompleteResponse" + } + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, + "/authorize": { + "get": { + "description": "OpenID Connect Authorize Endpoint", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "tags": [ + "oidc" + ], + "summary": "Authorize", + "parameters": [ + { + "type": "string", + "description": "OAuth scopes (space separated, must include openid)", + "name": "scope", + "in": "query" + }, + { + "type": "string", + "description": "Response type (e.g. code)", + "name": "response_type", + "in": "query" + }, + { + "type": "string", + "description": "Client ID", + "name": "client_id", + "in": "query" + }, + { + "type": "string", + "description": "Redirect URI", + "name": "redirect_uri", + "in": "query" + }, + { + "type": "string", + "description": "Opaque state value returned to the client", + "name": "state", + "in": "query" + }, + { + "type": "string", + "description": "Nonce for ID token replay protection", + "name": "nonce", + "in": "query" + }, + { + "type": "string", + "description": "PKCE code challenge", + "name": "code_challenge", + "in": "query" + }, + { + "type": "string", + "description": "PKCE code challenge method (S256 or plain)", + "name": "code_challenge_method", + "in": "query" + }, + { + "type": "string", + "description": "Prompt parameter (none, login, consent)", + "name": "prompt", + "in": "query" + }, + { + "type": "string", + "description": "Max authentication age in seconds", + "name": "max_age", + "in": "query" + }, + { + "type": "string", + "description": "OAuth scopes (space separated, must include openid)", + "name": "scope", + "in": "formData" + }, + { + "type": "string", + "description": "Response type (e.g. code)", + "name": "response_type", + "in": "formData" + }, + { + "type": "string", + "description": "Client ID", + "name": "client_id", + "in": "formData" + }, + { + "type": "string", + "description": "Redirect URI", + "name": "redirect_uri", + "in": "formData" + }, + { + "type": "string", + "description": "Opaque state value returned to the client", + "name": "state", + "in": "formData" + }, + { + "type": "string", + "description": "Nonce for ID token replay protection", + "name": "nonce", + "in": "formData" + }, + { + "type": "string", + "description": "PKCE code challenge", + "name": "code_challenge", + "in": "formData" + }, + { + "type": "string", + "description": "PKCE code challenge method (S256 or plain)", + "name": "code_challenge_method", + "in": "formData" + }, + { + "type": "string", + "description": "Prompt parameter (none, login, consent)", + "name": "prompt", + "in": "formData" + }, + { + "type": "string", + "description": "Max authentication age in seconds", + "name": "max_age", + "in": "formData" + } + ], + "responses": { + "302": { + "description": "Found" + } + } + }, + "post": { + "description": "OpenID Connect Authorize Endpoint", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "tags": [ + "oidc" + ], + "summary": "Authorize", + "parameters": [ + { + "type": "string", + "description": "OAuth scopes (space separated, must include openid)", + "name": "scope", + "in": "query" + }, + { + "type": "string", + "description": "Response type (e.g. code)", + "name": "response_type", + "in": "query" + }, + { + "type": "string", + "description": "Client ID", + "name": "client_id", + "in": "query" + }, + { + "type": "string", + "description": "Redirect URI", + "name": "redirect_uri", + "in": "query" + }, + { + "type": "string", + "description": "Opaque state value returned to the client", + "name": "state", + "in": "query" + }, + { + "type": "string", + "description": "Nonce for ID token replay protection", + "name": "nonce", + "in": "query" + }, + { + "type": "string", + "description": "PKCE code challenge", + "name": "code_challenge", + "in": "query" + }, + { + "type": "string", + "description": "PKCE code challenge method (S256 or plain)", + "name": "code_challenge_method", + "in": "query" + }, + { + "type": "string", + "description": "Prompt parameter (none, login, consent)", + "name": "prompt", + "in": "query" + }, + { + "type": "string", + "description": "Max authentication age in seconds", + "name": "max_age", + "in": "query" + }, + { + "type": "string", + "description": "OAuth scopes (space separated, must include openid)", + "name": "scope", + "in": "formData" + }, + { + "type": "string", + "description": "Response type (e.g. code)", + "name": "response_type", + "in": "formData" + }, + { + "type": "string", + "description": "Client ID", + "name": "client_id", + "in": "formData" + }, + { + "type": "string", + "description": "Redirect URI", + "name": "redirect_uri", + "in": "formData" + }, + { + "type": "string", + "description": "Opaque state value returned to the client", + "name": "state", + "in": "formData" + }, + { + "type": "string", + "description": "Nonce for ID token replay protection", + "name": "nonce", + "in": "formData" + }, + { + "type": "string", + "description": "PKCE code challenge", + "name": "code_challenge", + "in": "formData" + }, + { + "type": "string", + "description": "PKCE code challenge method (S256 or plain)", + "name": "code_challenge_method", + "in": "formData" + }, + { + "type": "string", + "description": "Prompt parameter (none, login, consent)", + "name": "prompt", + "in": "formData" + }, + { + "type": "string", + "description": "Max authentication age in seconds", + "name": "max_age", + "in": "formData" + } + ], + "responses": { + "302": { + "description": "Found" + } + } + } + }, + "/oidc/token": { + "post": { + "description": "OpenID Connect Token Endpoint", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "tags": [ + "oidc" + ], + "summary": "Token", + "parameters": [ + { + "type": "string", + "description": "Grant type (authorization_code or refresh_token)", + "name": "grant_type", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Authorization code (required for authorization_code grant)", + "name": "code", + "in": "query" + }, + { + "type": "string", + "description": "Redirect URI (must match the one from the authorize request)", + "name": "redirect_uri", + "in": "query" + }, + { + "type": "string", + "description": "Refresh token (required for refresh_token grant)", + "name": "refresh_token", + "in": "query" + }, + { + "type": "string", + "description": "Client ID (required if not using Basic auth)", + "name": "client_id", + "in": "query" + }, + { + "type": "string", + "description": "Client secret (required for confidential clients without Basic auth)", + "name": "client_secret", + "in": "query" + }, + { + "type": "string", + "description": "PKCE code verifier (required if code_challenge was sent)", + "name": "code_verifier", + "in": "query" + }, + { + "type": "string", + "description": "Grant type (authorization_code or refresh_token)", + "name": "grant_type", + "in": "formData" + }, + { + "type": "string", + "description": "Authorization code (required for authorization_code grant)", + "name": "code", + "in": "formData" + }, + { + "type": "string", + "description": "Redirect URI (must match the one from the authorize request)", + "name": "redirect_uri", + "in": "formData" + }, + { + "type": "string", + "description": "Refresh token (required for refresh_token grant)", + "name": "refresh_token", + "in": "formData" + }, + { + "type": "string", + "description": "Client ID (required if not using Basic auth)", + "name": "client_id", + "in": "formData" + }, + { + "type": "string", + "description": "Client secret (required for confidential clients without Basic auth)", + "name": "client_secret", + "in": "formData" + }, + { + "type": "string", + "description": "PKCE code verifier (required if code_challenge was sent)", + "name": "code_verifier", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service.TokenResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + } + } + } + }, + "/oidc/userinfo": { + "get": { + "description": "OpenID Connect Userinfo Endpoint", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "tags": [ + "oidc" + ], + "summary": "Userinfo", + "parameters": [ + { + "type": "string", + "description": "OpenID Connect Access Token", + "name": "access_token", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service.UserinfoResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + } + } + }, + "post": { + "description": "OpenID Connect Userinfo Endpoint", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "tags": [ + "oidc" + ], + "summary": "Userinfo", + "parameters": [ + { + "type": "string", + "description": "OpenID Connect Access Token", + "name": "access_token", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service.UserinfoResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/controller.OIDCErrorResponse" + } + } + } + } + } + }, + "definitions": { + "controller.ACRApp": { + "type": "object", + "properties": { + "appUrl": { + "type": "string" + }, + "cookieDomain": { + "type": "string" + }, + "subdomainsEnabled": { + "type": "boolean" + } + } + }, + "controller.ACRAuth": { + "type": "object", + "properties": { + "providers": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Provider" + } + } + } + }, + "controller.ACROAuth": { + "type": "object", + "properties": { + "autoRedirect": { + "type": "string" + } + } + }, + "controller.ACRUI": { + "type": "object", + "properties": { + "backgroundImage": { + "type": "string" + }, + "forgotPasswordMessage": { + "type": "string" + }, + "title": { + "type": "string" + }, + "warningsEnabled": { + "type": "boolean" + } + } + }, + "controller.AppContextResponse": { + "type": "object", + "properties": { + "app": { + "$ref": "#/definitions/controller.ACRApp" + }, + "auth": { + "$ref": "#/definitions/controller.ACRAuth" + }, + "message": { + "type": "string" + }, + "oauth": { + "$ref": "#/definitions/controller.ACROAuth" + }, + "status": { + "type": "integer" + }, + "ui": { + "$ref": "#/definitions/controller.ACRUI" + } + } + }, + "controller.AuthorizeCompleteResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "redirect_uri": { + "type": "string" + }, + "status": { + "type": "integer" + } + } + }, + "controller.OAuthURLSuccessResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "url": { + "type": "string" + } + } + }, + "controller.OIDCErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "controller.SimpleResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + } + }, + "controller.UCRAuth": { + "type": "object", + "properties": { + "authenticated": { + "type": "boolean" + }, + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "providerId": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "controller.UCROAuth": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "displayName": { + "type": "string" + } + } + }, + "controller.UCRTOTP": { + "type": "object", + "properties": { + "pending": { + "type": "boolean" + } + } + }, + "controller.UCRTailscale": { + "type": "object", + "properties": { + "nodeName": { + "type": "string" + } + } + }, + "controller.UserContextResponse": { + "type": "object", + "properties": { + "auth": { + "$ref": "#/definitions/controller.UCRAuth" + }, + "message": { + "type": "string" + }, + "oauth": { + "$ref": "#/definitions/controller.UCROAuth" + }, + "status": { + "type": "integer" + }, + "tailscale": { + "$ref": "#/definitions/controller.UCRTailscale" + }, + "totp": { + "$ref": "#/definitions/controller.UCRTOTP" + } + } + }, + "model.AddressClaim": { + "type": "object", + "properties": { + "country": { + "type": "string" + }, + "formatted": { + "type": "string" + }, + "locality": { + "type": "string" + }, + "postal_code": { + "type": "string" + }, + "region": { + "type": "string" + }, + "street_address": { + "type": "string" + } + } + }, + "model.Provider": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "oauth": { + "type": "boolean" + } + } + }, + "service.TokenResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "expires_in": { + "type": "integer" + }, + "id_token": { + "type": "string" + }, + "refresh_token": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "token_type": { + "type": "string" + } + } + }, + "service.UserinfoResponse": { + "type": "object", + "properties": { + "address": { + "$ref": "#/definitions/model.AddressClaim" + }, + "birthdate": { + "type": "string" + }, + "email": { + "type": "string" + }, + "email_verified": { + "type": "boolean" + }, + "family_name": { + "type": "string" + }, + "gender": { + "type": "string" + }, + "given_name": { + "type": "string" + }, + "groups": { + "type": "array", + "items": { + "type": "string" + } + }, + "locale": { + "type": "string" + }, + "middle_name": { + "type": "string" + }, + "name": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "phone_number": { + "type": "string" + }, + "phone_number_verified": { + "type": "boolean" + }, + "picture": { + "type": "string" + }, + "preferred_username": { + "type": "string" + }, + "profile": { + "type": "string" + }, + "sub": { + "type": "string" + }, + "updated_at": { + "type": "integer" + }, + "website": { + "type": "string" + }, + "zoneinfo": { + "type": "string" + } + } + } + } } \ No newline at end of file diff --git a/internal/swagger/swagger.yaml b/internal/swagger/swagger.yaml index 18df7bdc..88593164 100644 --- a/internal/swagger/swagger.yaml +++ b/internal/swagger/swagger.yaml @@ -1,4 +1,213 @@ -basePath: /api +basePath: / +definitions: + controller.ACRApp: + properties: + appUrl: + type: string + cookieDomain: + type: string + subdomainsEnabled: + type: boolean + type: object + controller.ACRAuth: + properties: + providers: + items: + $ref: '#/definitions/model.Provider' + type: array + type: object + controller.ACROAuth: + properties: + autoRedirect: + type: string + type: object + controller.ACRUI: + properties: + backgroundImage: + type: string + forgotPasswordMessage: + type: string + title: + type: string + warningsEnabled: + type: boolean + type: object + controller.AppContextResponse: + properties: + app: + $ref: '#/definitions/controller.ACRApp' + auth: + $ref: '#/definitions/controller.ACRAuth' + message: + type: string + oauth: + $ref: '#/definitions/controller.ACROAuth' + status: + type: integer + ui: + $ref: '#/definitions/controller.ACRUI' + type: object + controller.AuthorizeCompleteResponse: + properties: + message: + type: string + redirect_uri: + type: string + status: + type: integer + type: object + controller.OAuthURLSuccessResponse: + properties: + message: + type: string + status: + type: integer + url: + type: string + type: object + controller.OIDCErrorResponse: + properties: + error: + type: string + type: object + controller.SimpleResponse: + properties: + message: + type: string + status: + type: integer + type: object + controller.UCRAuth: + properties: + authenticated: + type: boolean + email: + type: string + name: + type: string + providerId: + type: string + username: + type: string + type: object + controller.UCROAuth: + properties: + active: + type: boolean + displayName: + type: string + type: object + controller.UCRTOTP: + properties: + pending: + type: boolean + type: object + controller.UCRTailscale: + properties: + nodeName: + type: string + type: object + controller.UserContextResponse: + properties: + auth: + $ref: '#/definitions/controller.UCRAuth' + message: + type: string + oauth: + $ref: '#/definitions/controller.UCROAuth' + status: + type: integer + tailscale: + $ref: '#/definitions/controller.UCRTailscale' + totp: + $ref: '#/definitions/controller.UCRTOTP' + type: object + model.AddressClaim: + properties: + country: + type: string + formatted: + type: string + locality: + type: string + postal_code: + type: string + region: + type: string + street_address: + type: string + type: object + model.Provider: + properties: + id: + type: string + name: + type: string + oauth: + type: boolean + type: object + service.TokenResponse: + properties: + access_token: + type: string + expires_in: + type: integer + id_token: + type: string + refresh_token: + type: string + scope: + type: string + token_type: + type: string + type: object + service.UserinfoResponse: + properties: + address: + $ref: '#/definitions/model.AddressClaim' + birthdate: + type: string + email: + type: string + email_verified: + type: boolean + family_name: + type: string + gender: + type: string + given_name: + type: string + groups: + items: + type: string + type: array + locale: + type: string + middle_name: + type: string + name: + type: string + nickname: + type: string + phone_number: + type: string + phone_number_verified: + type: boolean + picture: + type: string + preferred_username: + type: string + profile: + type: string + sub: + type: string + updated_at: + type: integer + website: + type: string + zoneinfo: + type: string + type: object info: contact: {} description: Swagger documentation for Tinyauth's API. @@ -7,5 +216,475 @@ info: url: https://github.com/tinyauthapp/tinyauth/blob/main/LICENSE title: Tinyauth API version: development -paths: {} +paths: + /api/context/app: + get: + description: Get the app context + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.AppContextResponse' + summary: App context + tags: + - context + /api/context/user: + get: + description: Get the user context + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.UserContextResponse' + summary: User context + tags: + - context + /api/healthz: + get: + description: Check if the server is up and running + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.SimpleResponse' + summary: Healthcheck + tags: + - health + head: + description: Check if the server is up and running + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.SimpleResponse' + summary: Healthcheck + tags: + - health + /api/oauth/callback/{id}: + get: + description: Callback URL for OAuth providers + parameters: + - description: Provider ID + in: path + name: id + required: true + type: string + - description: State + in: query + name: code + required: true + type: string + - description: Code + in: query + name: state + required: true + type: string + responses: + "302": + description: Found + summary: OAuth Callback + tags: + - oauth + /api/oauth/url/{id}: + get: + description: Get an OAuth URL for the specified provider + parameters: + - description: Provider ID + in: path + name: id + required: true + type: string + - description: Login for + in: query + name: login_for + type: string + - description: OpenID Connect Ticket + in: query + name: oidc_ticket + type: string + - description: OpenID Connect Scope + in: query + name: oidc_scope + type: string + - description: OpenID Connect Name + in: query + name: oidc_name + type: string + - description: Redirect URI + in: query + name: redirect_uri + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.OAuthURLSuccessResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/controller.SimpleResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/controller.SimpleResponse' + summary: OAuth URL + tags: + - oauth + /api/oidc/authorize-complete: + post: + consumes: + - application/json + description: Internal endpoint for the completion of the OpenID Connect authorization + flow + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controller.AuthorizeCompleteResponse' + "500": + description: Internal Server Error + summary: Authorize Complete + tags: + - oidc + /authorize: + get: + consumes: + - application/x-www-form-urlencoded + description: OpenID Connect Authorize Endpoint + parameters: + - description: OAuth scopes (space separated, must include openid) + in: query + name: scope + type: string + - description: Response type (e.g. code) + in: query + name: response_type + type: string + - description: Client ID + in: query + name: client_id + type: string + - description: Redirect URI + in: query + name: redirect_uri + type: string + - description: Opaque state value returned to the client + in: query + name: state + type: string + - description: Nonce for ID token replay protection + in: query + name: nonce + type: string + - description: PKCE code challenge + in: query + name: code_challenge + type: string + - description: PKCE code challenge method (S256 or plain) + in: query + name: code_challenge_method + type: string + - description: Prompt parameter (none, login, consent) + in: query + name: prompt + type: string + - description: Max authentication age in seconds + in: query + name: max_age + type: string + - description: OAuth scopes (space separated, must include openid) + in: formData + name: scope + type: string + - description: Response type (e.g. code) + in: formData + name: response_type + type: string + - description: Client ID + in: formData + name: client_id + type: string + - description: Redirect URI + in: formData + name: redirect_uri + type: string + - description: Opaque state value returned to the client + in: formData + name: state + type: string + - description: Nonce for ID token replay protection + in: formData + name: nonce + type: string + - description: PKCE code challenge + in: formData + name: code_challenge + type: string + - description: PKCE code challenge method (S256 or plain) + in: formData + name: code_challenge_method + type: string + - description: Prompt parameter (none, login, consent) + in: formData + name: prompt + type: string + - description: Max authentication age in seconds + in: formData + name: max_age + type: string + responses: + "302": + description: Found + summary: Authorize + tags: + - oidc + post: + consumes: + - application/x-www-form-urlencoded + description: OpenID Connect Authorize Endpoint + parameters: + - description: OAuth scopes (space separated, must include openid) + in: query + name: scope + type: string + - description: Response type (e.g. code) + in: query + name: response_type + type: string + - description: Client ID + in: query + name: client_id + type: string + - description: Redirect URI + in: query + name: redirect_uri + type: string + - description: Opaque state value returned to the client + in: query + name: state + type: string + - description: Nonce for ID token replay protection + in: query + name: nonce + type: string + - description: PKCE code challenge + in: query + name: code_challenge + type: string + - description: PKCE code challenge method (S256 or plain) + in: query + name: code_challenge_method + type: string + - description: Prompt parameter (none, login, consent) + in: query + name: prompt + type: string + - description: Max authentication age in seconds + in: query + name: max_age + type: string + - description: OAuth scopes (space separated, must include openid) + in: formData + name: scope + type: string + - description: Response type (e.g. code) + in: formData + name: response_type + type: string + - description: Client ID + in: formData + name: client_id + type: string + - description: Redirect URI + in: formData + name: redirect_uri + type: string + - description: Opaque state value returned to the client + in: formData + name: state + type: string + - description: Nonce for ID token replay protection + in: formData + name: nonce + type: string + - description: PKCE code challenge + in: formData + name: code_challenge + type: string + - description: PKCE code challenge method (S256 or plain) + in: formData + name: code_challenge_method + type: string + - description: Prompt parameter (none, login, consent) + in: formData + name: prompt + type: string + - description: Max authentication age in seconds + in: formData + name: max_age + type: string + responses: + "302": + description: Found + summary: Authorize + tags: + - oidc + /oidc/token: + post: + consumes: + - application/x-www-form-urlencoded + description: OpenID Connect Token Endpoint + parameters: + - description: Grant type (authorization_code or refresh_token) + in: query + name: grant_type + required: true + type: string + - description: Authorization code (required for authorization_code grant) + in: query + name: code + type: string + - description: Redirect URI (must match the one from the authorize request) + in: query + name: redirect_uri + type: string + - description: Refresh token (required for refresh_token grant) + in: query + name: refresh_token + type: string + - description: Client ID (required if not using Basic auth) + in: query + name: client_id + type: string + - description: Client secret (required for confidential clients without Basic + auth) + in: query + name: client_secret + type: string + - description: PKCE code verifier (required if code_challenge was sent) + in: query + name: code_verifier + type: string + - description: Grant type (authorization_code or refresh_token) + in: formData + name: grant_type + type: string + - description: Authorization code (required for authorization_code grant) + in: formData + name: code + type: string + - description: Redirect URI (must match the one from the authorize request) + in: formData + name: redirect_uri + type: string + - description: Refresh token (required for refresh_token grant) + in: formData + name: refresh_token + type: string + - description: Client ID (required if not using Basic auth) + in: formData + name: client_id + type: string + - description: Client secret (required for confidential clients without Basic + auth) + in: formData + name: client_secret + type: string + - description: PKCE code verifier (required if code_challenge was sent) + in: formData + name: code_verifier + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service.TokenResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/controller.OIDCErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/controller.OIDCErrorResponse' + summary: Token + tags: + - oidc + /oidc/userinfo: + get: + consumes: + - application/x-www-form-urlencoded + description: OpenID Connect Userinfo Endpoint + parameters: + - description: OpenID Connect Access Token + in: formData + name: access_token + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service.UserinfoResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/controller.OIDCErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/controller.OIDCErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/controller.OIDCErrorResponse' + summary: Userinfo + tags: + - oidc + post: + consumes: + - application/x-www-form-urlencoded + description: OpenID Connect Userinfo Endpoint + parameters: + - description: OpenID Connect Access Token + in: formData + name: access_token + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service.UserinfoResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/controller.OIDCErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/controller.OIDCErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/controller.OIDCErrorResponse' + summary: Userinfo + tags: + - oidc swagger: "2.0"