From 3487fd392e770c3e4c7af9aa5ef8e3e25b9a73eb Mon Sep 17 00:00:00 2001 From: Amir Zarrinkafsh Date: Sun, 3 Jan 2021 15:28:46 +1100 Subject: [PATCH] [FEATURE] Add API docs and swagger-ui (#1544) * [FEATURE] Add API docs and swagger-ui This change will serve out swagger-ui at the `/api/` root path. * Update descriptions and summaries in API spec * Utilise frontend assets from unit testing for Docker build steps * Fix tag for /api/user/* endpoints * Fix response schema for /api/user/info/2fa_method * Template and inject the session name during runtime into swagger-ui This change also factorises and renames index.go into template.go, this can now be generically utilised to template any file. * Fix integration tests * Add U2F endpoints * Change swagger directory to api This change is to more closely conform to the golang-standards project layout. * Add authentication for u2f endpoints * Modify u2f endpoint descriptions * Rename and fix u2f 2fa sign endpoints * Fix request body for /api/secondfactor/u2f/sign endpoint Co-authored-by: James Elliott --- .buildkite/hooks/pre-command | 6 + .buildkite/pipeline.sh | 1 + .buildkite/steps/buildimages.sh | 2 + Dockerfile | 13 +- Dockerfile.arm32v7 | 13 +- Dockerfile.arm64v8 | 13 +- Dockerfile.coverage | 3 +- api/index.html | 60 +++ api/openapi.yml | 752 +++++++++++++++++++++++++++++ cmd/authelia-scripts/cmd_build.go | 62 ++- cmd/authelia-scripts/const.go | 1 + internal/server/const.go | 3 + internal/server/index.go | 55 --- internal/server/public_html.gen.go | 2 +- internal/server/server.go | 10 +- internal/server/template.go | 65 +++ web/.gitignore | 1 + 17 files changed, 952 insertions(+), 110 deletions(-) create mode 100644 api/index.html create mode 100644 api/openapi.yml delete mode 100644 internal/server/index.go create mode 100644 internal/server/template.go diff --git a/.buildkite/hooks/pre-command b/.buildkite/hooks/pre-command index 2a0229306..399bd8ed4 100755 --- a/.buildkite/hooks/pre-command +++ b/.buildkite/hooks/pre-command @@ -14,6 +14,12 @@ if [[ $BUILDKITE_LABEL =~ ":selenium:" ]]; then docker tag authelia/authelia authelia:dist fi +if [[ $BUILDKITE_LABEL =~ ":docker: Build Image" ]] && [[ "${ARCH}" != "coverage" ]]; then + echo "--- :react: :swagger: Extract frontend assets" + buildkite-agent artifact download "authelia-public_html.tar.gz" . + tar xzf authelia-public_html.tar.gz +fi + if [[ $BUILDKITE_LABEL =~ ":docker: Deploy Image" ]]; then buildkite-agent artifact download "authelia-image-${ARCH}*" . zstdcat authelia-image-"${ARCH}".tar.zst | docker load diff --git a/.buildkite/pipeline.sh b/.buildkite/pipeline.sh index f7f485336..dbf1db610 100755 --- a/.buildkite/pipeline.sh +++ b/.buildkite/pipeline.sh @@ -37,6 +37,7 @@ steps: artifact_paths: - "authelia-public_html.tar.gz" - "authelia-public_html.tar.gz.sha256" + key: "unit-test" if: build.env("CI_BYPASS") != "true" - wait: diff --git a/.buildkite/steps/buildimages.sh b/.buildkite/steps/buildimages.sh index 8ad025f13..65aa2cfb6 100755 --- a/.buildkite/steps/buildimages.sh +++ b/.buildkite/steps/buildimages.sh @@ -17,6 +17,8 @@ if [[ "${BUILD_ARCH}" != "coverage" ]]; then cat << EOF - "authelia-${BUILD_OS}-${BUILD_ARCH}.tar.gz" - "authelia-${BUILD_OS}-${BUILD_ARCH}.tar.gz.sha256" + depends_on: + - "unit-test" EOF fi cat << EOF diff --git a/Dockerfile b/Dockerfile index 7a059d2a6..2826bc3a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,3 @@ -# ======================================== -# ===== Build image for the frontend ===== -# ======================================== -FROM node:15-alpine AS builder-frontend - -WORKDIR /node/src/app -COPY web . - -# Install the dependencies and build -RUN yarn install --frozen-lockfile && INLINE_RUNTIME_CHUNK=false yarn build - # ======================================= # ===== Build image for the backend ===== # ======================================= @@ -23,12 +12,12 @@ RUN apk --no-cache add gcc musl-dev WORKDIR /go/src/app COPY go.mod go.sum config.template.yml ./ -COPY --from=builder-frontend /node/src/app/build public_html RUN go mod download COPY cmd cmd COPY internal internal +COPY public_html public_html # Prepare static files to be embedded in Go binary RUN go get -u aletheia.icu/broccoli && \ diff --git a/Dockerfile.arm32v7 b/Dockerfile.arm32v7 index bb5d1a8b2..67132b4a2 100644 --- a/Dockerfile.arm32v7 +++ b/Dockerfile.arm32v7 @@ -1,14 +1,3 @@ -# ======================================== -# ===== Build image for the frontend ===== -# ======================================== -FROM node:15-alpine AS builder-frontend - -WORKDIR /node/src/app -COPY web . - -# Install the dependencies and build -RUN yarn install --frozen-lockfile && INLINE_RUNTIME_CHUNK=false yarn build - # ======================================= # ===== Build image for the backend ===== # ======================================= @@ -26,12 +15,12 @@ RUN apk --no-cache add curl && \ WORKDIR /go/src/app COPY go.mod go.sum config.template.yml ./ -COPY --from=builder-frontend /node/src/app/build public_html RUN go mod download COPY cmd cmd COPY internal internal +COPY public_html public_html # Prepare static files to be embedded in Go binary RUN go get -u aletheia.icu/broccoli && \ diff --git a/Dockerfile.arm64v8 b/Dockerfile.arm64v8 index a389b80ed..a4f655cbe 100644 --- a/Dockerfile.arm64v8 +++ b/Dockerfile.arm64v8 @@ -1,14 +1,3 @@ -# ======================================== -# ===== Build image for the frontend ===== -# ======================================== -FROM node:15-alpine AS builder-frontend - -WORKDIR /node/src/app -COPY web . - -# Install the dependencies and build -RUN yarn install --frozen-lockfile && INLINE_RUNTIME_CHUNK=false yarn build - # ======================================= # ===== Build image for the backend ===== # ======================================= @@ -26,12 +15,12 @@ RUN apk --no-cache add curl && \ WORKDIR /go/src/app COPY go.mod go.sum config.template.yml ./ -COPY --from=builder-frontend /node/src/app/build public_html RUN go mod download COPY cmd cmd COPY internal internal +COPY public_html public_html # Prepare static files to be embedded in Go binary RUN go get -u aletheia.icu/broccoli && \ diff --git a/Dockerfile.coverage b/Dockerfile.coverage index 2be641565..518e7755b 100644 --- a/Dockerfile.coverage +++ b/Dockerfile.coverage @@ -7,7 +7,8 @@ WORKDIR /node/src/app COPY web . # Install the dependencies and build -RUN yarn install --frozen-lockfile && INLINE_RUNTIME_CHUNK=false yarn coverage +RUN yarn install --frozen-lockfile && INLINE_RUNTIME_CHUNK=false yarn coverage && \ +mkdir -p /node/src/app/build/api && cd /node/src/app/build/api/ && touch index.html openapi.yml # ======================================= # ===== Build image for the backend ===== diff --git a/api/index.html b/api/index.html new file mode 100644 index 000000000..070b716e4 --- /dev/null +++ b/api/index.html @@ -0,0 +1,60 @@ + + + + + + Swagger UI + + + + + + + +
+ + + + + + diff --git a/api/openapi.yml b/api/openapi.yml new file mode 100644 index 000000000..646f5dd6a --- /dev/null +++ b/api/openapi.yml @@ -0,0 +1,752 @@ +--- +openapi: 3.0.0 +info: + title: Authelia API + description: Authelia is an open-source authentication and authorization server providing 2-factor authentication and single sign-on (SSO) for your applications via a web portal. + contact: + name: Authelia Support + url: https://github.com/authelia/authelia#contact-options + email: team@authelia.com + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 + version: 1.0.0 +tags: + - name: State + description: Configuration, health and state endpoints + - name: Authentication + description: Authentication and verification endpoints + - name: Password Reset + description: Password reset endpoints + - name: User Information + description: User configuration endpoints + - name: Second Factor + description: TOTP, U2F and Duo endpoints +paths: + /api/configuration: + get: + tags: + - State + summary: Application Configuration + description: The configuration endpoint provides detailed information including available second factor methods, if any second factor policies exist and the TOTP period configuration. + responses: + "200": + description: Successful Operation + content: + application/json: + schema: + $ref: '#/components/schemas/handlers.configuration.ConfigurationBody' + "403": + description: Forbidden + security: + - authelia_auth: [ ] + /api/health: + get: + tags: + - State + summary: Application Health + description: The health check endpoint provides information about the health of Authelia. + responses: + "200": + description: Successful Operation + content: + application/json: + schema: + $ref: '#/components/schemas/middlewares.OkResponse' + /api/state: + get: + tags: + - State + summary: User Application State + description: The state endpoint provides detailed information including the user, current authenticate level and Authelia's configured default redirection URL. + responses: + "200": + description: Successful Operation + content: + application/json: + schema: + $ref: '#/components/schemas/handlers.StateResponse' + /api/verify: + get: + tags: + - Authentication + summary: Verification + description: The verify endpoint provides the ability to verify if a user has the necessary permissions to access a specified domain. + parameters: + - name: X-Original-URL + in: header + description: Redirection URL + required: true + style: simple + explode: true + schema: + type: string + responses: + "200": + description: Successful Operation + headers: + remote-user: + description: Username + schema: + type: string + example: john + remote-name: + description: Name + schema: + type: string + example: John Doe + remote-email: + description: Email + schema: + type: string + example: john.doe@authelia.com + remote-groups: + description: Comma separated list of Groups + schema: + type: string + example: admin,devs + "401": + description: Unauthorized + security: + - authelia_auth: [] + head: + tags: + - Authentication + summary: Verification + description: The verify endpoint provides the ability to verify if a user has the necessary permissions to access a specified domain. + parameters: + - name: X-Original-URL + in: header + description: Redirection URL + required: true + style: simple + explode: true + schema: + type: string + responses: + "200": + description: Successful Operation + headers: + remote-user: + description: Username + schema: + type: string + example: john + remote-name: + description: Name + schema: + type: string + example: John Doe + remote-email: + description: Email + schema: + type: string + example: john.doe@authelia.com + remote-groups: + description: Comma separated list of Groups + schema: + type: string + example: admin,devs + "401": + description: Unauthorized + security: + - authelia_auth: [] + /api/firstfactor: + post: + tags: + - Authentication + summary: Login + description: The firstfactor endpoint allows a user to login and generates an authentication cookie for authorization. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/handlers.firstFactorRequestBody' + responses: + "200": + description: Successful Operation + headers: + Set-Cookie: + style: simple + explode: false + schema: + type: string + example: authelia_session=kTTCSLupEUirZVfLeZTijezewFQnNOgs; Path=/ + content: + application/json: + schema: + $ref: '#/components/schemas/handlers.redirectResponse' + "401": + description: Unauthorized + security: + - authelia_auth: [] + /api/logout: + post: + tags: + - Authentication + summary: Logout + description: The logout endpoint allows a user to logout and destroy a sesssion. + responses: + "200": + description: Successful Operation + content: + application/json: + schema: + $ref: '#/components/schemas/middlewares.OkResponse' + security: + - authelia_auth: [ ] + /api/reset-password/identity/start: + post: + tags: + - Password Reset + summary: Identity Verification Token Creation + description: "This endpoint is step 1 of 3 in the password reset process.\n\nIt validates the user session and sends the user an email with a token and a link to reset their password. This step also generates a session cookie for the rest of the process.\n\nThe same session cookie must be used for all steps in this process." + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/handlers.resetPasswordStep1RequestBody' + responses: + "200": + description: Successful Operation + content: + application/json: + schema: + $ref: '#/components/schemas/middlewares.OkResponse' + security: + - authelia_auth: [] + /api/reset-password/identity/finish: + post: + tags: + - Password Reset + summary: Identity Verification Token Validation + description: "This endpoint is step 2 of 3 in the password reset process.\n\nIt validates the user session and reset token.\n\nThe same session cookie must be used for all steps in this process." + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/middlewares.IdentityVerificationFinishBody' + responses: + "200": + description: Successful Operation + content: + application/json: + schema: + $ref: '#/components/schemas/middlewares.OkResponse' + security: + - authelia_auth: [] + /api/reset-password: + post: + tags: + - Password Reset + summary: Password Reset + description: "This endpoint is step 3 of 3 in the password reset process.\n\nIt validates the user session and changes the password.\n\nThe same session cookie must be used for all steps in this process." + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/handlers.resetPasswordStep2RequestBody' + responses: + "200": + description: Successful Operation + content: + application/json: + schema: + $ref: '#/components/schemas/middlewares.OkResponse' + security: + - authelia_auth: [] + /api/user/info: + get: + tags: + - User Information + summary: User Configuration + description: The user info endpoint provides detailed information including a users display name, preferred and registered second factor method(s). + responses: + "200": + description: Successful Operation + content: + application/json: + schema: + $ref: '#/components/schemas/handlers.UserInfo' + "403": + description: Forbidden + security: + - authelia_auth: [ ] + /api/user/info/2fa_method: + post: + tags: + - User Information + summary: User Configuration + description: The user info 2fa_method endpoint sets the users preferred second factor method. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/handlers.UserInfo.MethodBody' + responses: + "200": + description: Successful Operation + content: + application/json: + schema: + $ref: '#/components/schemas/middlewares.OkResponse' + "403": + description: Forbidden + security: + - authelia_auth: [ ] + /api/secondfactor/totp/identity/start: + post: + tags: + - Second Factor + summary: Identity Verification TOTP Token Creation + description: "This endpoint performs identity verification to begin the TOTP device registration process.\n\nThe session generated from this endpoint must be utilised for the subsequent step in the `/api/secondfactor/totp/identity/finish` endpoint." + responses: + "200": + description: Successful Operation + content: + application/json: + schema: + $ref: '#/components/schemas/middlewares.OkResponse' + security: + - authelia_auth: [] + /api/secondfactor/totp/identity/finish: + post: + tags: + - Second Factor + summary: Identity Verification TOTP Token Validation and Device Creation + description: "This endpoint performs identity and token verification, upon success also generates TOTP device secret and registers said device.\n\nThe session cookie generated from the `/api/secondfactor/totp/identity/start` endpoint must be utilised for the step here" + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/middlewares.IdentityVerificationFinishBody' + responses: + "200": + description: Successful Operation + content: + application/json: + schema: + $ref: '#/components/schemas/handlers.TOTPKeyResponse' + security: + - authelia_auth: [] + /api/secondfactor/totp: + post: + tags: + - Second Factor + summary: Second Factor Authentication - TOTP + description: "This endpoint performs second factor authentication with a TOTP key." + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/handlers.signTOTPRequestBody' + responses: + "200": + description: Successful Operation + content: + application/json: + schema: + $ref: '#/components/schemas/handlers.redirectResponse' + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/middlewares.ErrorResponse' + security: + - authelia_auth: [] + /api/secondfactor/u2f/sign_request: + post: + tags: + - Second Factor + summary: Second Factor Authentication - U2F (Request) + description: "This endpoint starts the second factor authentication process with the U2F key." + responses: + "200": + description: Successful Operation + content: + application/json: + schema: + $ref: '#/components/schemas/u2f.WebSignRequest' + "401": + description: Unauthorized + security: + - authelia_auth: [] + /api/secondfactor/u2f/sign: + post: + tags: + - Second Factor + summary: Second Factor Authentication - U2F + description: "This endpoint completes second factor authentication with a U2F key." + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/handlers.signU2FRequestBody" + responses: + "200": + description: Successful Operation + content: + application/json: + schema: + $ref: '#/components/schemas/handlers.redirectResponse' + "401": + description: Unauthorized + security: + - authelia_auth: [] + /api/secondfactor/u2f/identity/start: + post: + tags: + - Second Factor + summary: Identity Verification U2F Token Creation + description: "This endpoint performs identity verification to begin the U2F device registration process.\n\nThe session generated from this endpoint must be utilised for the subsequent steps in the `/api/secondfactor/u2f/identity/finish` and `/api/secondfactor/u2f/register` endpoints." + responses: + "200": + description: Successful Operation + content: + application/json: + schema: + $ref: '#/components/schemas/middlewares.OkResponse' + security: + - authelia_auth: [] + /api/secondfactor/u2f/identity/finish: + post: + tags: + - Second Factor + summary: Identity Verification U2F Token Validation + description: "This endpoint performs identity and token verification, upon success generates a U2F device registration challenge.\n\nThe session cookie generated from the `/api/secondfactor/u2f/identity/start` endpoint must be utilised for the subsequent steps here and in the `/api/secondfactor/u2f/register` endpoint." + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/middlewares.IdentityVerificationFinishBody' + responses: + "200": + description: Successful Operation + content: + application/json: + schema: + $ref: '#/components/schemas/u2f.WebRegisterRequest' + security: + - authelia_auth: [] + /api/secondfactor/u2f/register: + post: + tags: + - Second Factor + summary: U2F Device Registration + description: "This endpoint performs U2F device registration." + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/u2f.RegisterResponse' + responses: + "200": + description: Successful Operation + content: + application/json: + schema: + $ref: '#/components/schemas/middlewares.OkResponse' + security: + - authelia_auth: [] + /api/secondfactor/duo: + post: + tags: + - Second Factor + summary: Second Factor Authentication - Duo Mobile Push + description: "This endpoint performs second factor authentication with a Duo Mobile Push." + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/handlers.signDuoRequestBody' + responses: + "200": + description: Successful Operation + content: + application/json: + schema: + $ref: '#/components/schemas/handlers.redirectResponse' + "401": + description: Unauthorized + security: + - authelia_auth: [] +components: + schemas: + handlers.configuration.ConfigurationBody: + type: object + properties: + status: + type: string + example: OK + data: + type: object + properties: + available_methods: + type: array + items: + type: string + example: [totp, u2f, mobile_push] + second_factor_enabled: + type: boolean + description: If second factor is enabled. + totp_period: + type: integer + example: 30 + handlers.firstFactorRequestBody: + required: + - username + - password + type: object + properties: + username: + type: string + example: john + password: + type: string + example: password + targetURL: + type: string + example: https://home.example.com + keepMeLoggedIn: + type: boolean + example: true + handlers.redirectResponse: + type: object + properties: + status: + type: string + example: OK + data: + type: object + properties: + redirect: + type: string + example: https://home.example.com + handlers.resetPasswordStep1RequestBody: + required: + - username + type: object + properties: + username: + type: string + example: john + handlers.resetPasswordStep2RequestBody: + required: + - password + type: object + properties: + password: + type: string + example: password + handlers.signDuoRequestBody: + type: object + properties: + targetURL: + type: string + example: https://secure.example.com + handlers.signTOTPRequestBody: + type: object + properties: + token: + type: string + example: "123456" + targetURL: + type: string + example: https://secure.example.com + handlers.signU2FRequestBody: + type: object + properties: + targetURL: + type: string + example: https://secure.example.com + signResponse: + type: object + properties: + clientData: + type: string + example: 6prxyWqSsR6MXFchtQRzwZVTedWq7Zdc6XreLt6xRDXKeqJN7vzKAfYcKwRD3AT57bP4YFL4hbxat4LUysBNss + keyHandle: + type: string + example: pWgBrwr9meS5vArdffPtD4Px6AqZS7MfGEf776Rz438ujwHjeXwQEZuK53sRQ4wjeAgRCW4wX9VRj8dyKjc273 + signatureData: + type: string + example: p3Pe26B6T2E7EEEc59P4p869qwxy8cQAU2ttyGtGrQHb4XL2ZxCpWrawsSHNSTRZQd7jEW59Y3Ku9vSNRzj7Ly + handlers.StateResponse: + type: object + properties: + status: + type: string + example: OK + data: + type: object + properties: + username: + type: string + example: john + authentication_level: + type: integer + example: 1 + default_redirection_url: + type: string + example: https://home.example.com + handlers.TOTPKeyResponse: + type: object + properties: + status: + type: string + example: OK + data: + type: object + properties: + base32_secret: + type: string + example: 5ZH7Y5CTFWOXN7EOLGBMMXADRNQFHVUDZSYKCN5HMFAIRSLAWY3Q + otpauth_url: + type: string + example: otpauth://totp/auth.example.com:john?algorithm=SHA1&digits=6&issuer=auth.example.com&period=30&secret=5ZH7Y5CTFWOXN7EOLGBMMXADRNQFHVUDZSYKCN5HMFAIRSLAWY3Q + handlers.UserInfo: + type: object + properties: + status: + type: string + example: OK + data: + type: object + properties: + display_name: + type: string + example: John Doe + method: + type: string + enum: [totp, u2f, mobile_push] + example: totp + has_u2f: + type: boolean + example: false + has_totp: + type: boolean + example: true + handlers.UserInfo.MethodBody: + required: + - method + type: object + properties: + method: + type: string + enum: [totp, u2f, mobile_push] + example: totp + middlewares.ErrorResponse: + type: object + properties: + status: + type: string + example: KO + message: + type: string + example: Authentication failed, please retry later. + middlewares.IdentityVerificationFinishBody: + required: + - token + type: object + properties: + token: + type: string + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDc5MjU1OTYsImlzcyI6IkF1dGhlbGlhIiwiYWN0aW9uIjoiUmVzZXRQYXNzd29yZCIsInVzZXJuYW1lIjoiQW1pciJ9.636yqRrUCGCe4jsMCsonleX5CYWHncYqZum-YYb6VaY + middlewares.OkResponse: + type: object + properties: + status: + type: string + example: OK + data: + type: object + u2f.RegisterResponse: + type: object + properties: + version: + type: string + registrationData: + type: string + clientData: + type: string + u2f.WebRegisterRequest: + type: object + properties: + status: + type: string + example: OK + data: + type: object + properties: + appId: + type: string + example: https://auth.example.com + registerRequests: + type: array + items: + type: object + properties: + version: + type: string + example: U2F_V2 + challenge: + type: string + example: XGYKUzSmTpM1KxxpekArviW0w0OU2pwwRAocgn8TkVQ + registeredKeys: + type: array + items: + type: object + properties: + appId: + type: string + example: https://auth.example.com + version: + type: string + example: U2F_V2 + keyHandle: + type: string + example: pWgBrwr9meS5vArdffPtD4Px6AqZS7MfGEf776Rz438ujwHjeXwQEZuK53sRQ4wjeAgRCW4wX9VRj8dyKjc273 + u2f.WebSignRequest: + type: object + properties: + status: + type: string + example: OK + data: + type: object + properties: + appId: + type: string + example: https://auth.example.com + challenge: + type: string + example: XGYKUzSmTpM1KxxpekArviW0w0OU2pwwRAocgn8TkVQ + registeredKeys: + type: array + items: + type: object + properties: + appId: + type: string + example: https://auth.example.com + version: + type: string + example: U2F_V2 + keyHandle: + type: string + example: pWgBrwr9meS5vArdffPtD4Px6AqZS7MfGEf776Rz438ujwHjeXwQEZuK53sRQ4wjeAgRCW4wX9VRj8dyKjc273 + securitySchemes: + authelia_auth: + type: apiKey + name: "{{.Session}}" + in: cookie \ No newline at end of file diff --git a/cmd/authelia-scripts/cmd_build.go b/cmd/authelia-scripts/cmd_build.go index 21aa96695..ac95f6b89 100644 --- a/cmd/authelia-scripts/cmd_build.go +++ b/cmd/authelia-scripts/cmd_build.go @@ -17,32 +17,63 @@ func buildAutheliaBinary() { "GOOS=linux", "GOARCH=amd64", "CGO_ENABLED=1") err := cmd.Run() - if err != nil { - panic(err) + log.Fatal(err) } } func buildFrontend() { - // Install npm dependencies. cmd := utils.CommandWithStdout("yarn", "install") cmd.Dir = webDirectory - if err := cmd.Run(); err != nil { + err := cmd.Run() + if err != nil { log.Fatal(err) } - // Then build the frontend. cmd = utils.CommandWithStdout("yarn", "build") cmd.Dir = webDirectory cmd.Env = append(os.Environ(), "INLINE_RUNTIME_CHUNK=false") - if err := cmd.Run(); err != nil { + err = cmd.Run() + if err != nil { log.Fatal(err) } - if err := os.Rename("web/build", "./public_html"); err != nil { + err = os.Rename("web/build", "./public_html") + if err != nil { + log.Fatal(err) + } +} + +func buildSwagger() { + swaggerVer := "3.38.0" + cmd := utils.CommandWithStdout("bash", "-c", "wget -q https://github.com/swagger-api/swagger-ui/archive/v"+swaggerVer+".tar.gz -O ./v"+swaggerVer+".tar.gz") + + err := cmd.Run() + if err != nil { + log.Fatal(err) + } + + cmd = utils.CommandWithStdout("cp", "-r", "api", "public_html") + + err = cmd.Run() + if err != nil { + log.Fatal(err) + } + + cmd = utils.CommandWithStdout("tar", "-C", swaggerDirectory, "--exclude=index.html", "--strip-components=2", "-xf", "v"+swaggerVer+".tar.gz", "swagger-ui-"+swaggerVer+"/dist") + + err = cmd.Run() + if err != nil { + log.Fatal(err) + } + + cmd = utils.CommandWithStdout("rm", "./v"+swaggerVer+".tar.gz") + + err = cmd.Run() + if err != nil { log.Fatal(err) } } @@ -51,30 +82,28 @@ func generateEmbeddedAssets() { cmd := utils.CommandWithStdout("go", "get", "-u", "aletheia.icu/broccoli") err := cmd.Run() - if err != nil { - panic(err) + log.Fatal(err) } cmd = utils.CommandWithStdout("go", "generate", ".") cmd.Dir = "internal/configuration" err = cmd.Run() - if err != nil { - panic(err) + log.Fatal(err) } cmd = utils.CommandWithStdout("go", "generate", ".") cmd.Dir = "internal/server" err = cmd.Run() - if err != nil { - panic(err) + log.Fatal(err) } - if err := os.Rename("./public_html", OutputDir+"/public_html"); err != nil { + err = os.Rename("./public_html", OutputDir+"/public_html") + if err != nil { log.Fatal(err) } } @@ -89,12 +118,15 @@ func Build(cobraCmd *cobra.Command, args []string) { err := os.MkdirAll(OutputDir, os.ModePerm) if err != nil { - panic(err) + log.Fatal(err) } log.Debug("Building Authelia frontend...") buildFrontend() + log.Debug("Building swagger-ui frontend...") + buildSwagger() + log.Debug("Building Authelia Go binary...") generateEmbeddedAssets() buildAutheliaBinary() diff --git a/cmd/authelia-scripts/const.go b/cmd/authelia-scripts/const.go index efb83cde0..fc041fdf4 100644 --- a/cmd/authelia-scripts/const.go +++ b/cmd/authelia-scripts/const.go @@ -12,4 +12,5 @@ var IntermediateDockerImageName = "authelia:dist" const masterTag = "master" const stringFalse = "false" const stringTrue = "true" +const swaggerDirectory = "public_html/api" const webDirectory = "web" diff --git a/internal/server/const.go b/internal/server/const.go index d092ebf7d..00968a810 100644 --- a/internal/server/const.go +++ b/internal/server/const.go @@ -1,3 +1,6 @@ package server +const apiFile = "openapi.yml" +const indexFile = "index.html" + const dev = "dev" diff --git a/internal/server/index.go b/internal/server/index.go deleted file mode 100644 index ad4f36ac0..000000000 --- a/internal/server/index.go +++ /dev/null @@ -1,55 +0,0 @@ -package server - -import ( - "fmt" - "io/ioutil" - "os" - "text/template" - - "github.com/valyala/fasthttp" - - "github.com/authelia/authelia/internal/logging" - "github.com/authelia/authelia/internal/utils" -) - -var alphaNumericRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") - -// ServeIndex serve the index.html file with nonce generated for supporting -// restrictive CSP while using material-ui from the embedded virtual filesystem. -//go:generate broccoli -src ../../public_html -o public_html -func ServeIndex(publicDir, base, rememberMe, resetPassword string) fasthttp.RequestHandler { - f, err := br.Open(publicDir + "/index.html") - if err != nil { - logging.Logger().Fatalf("Unable to open index.html: %v", err) - } - - b, err := ioutil.ReadAll(f) - if err != nil { - logging.Logger().Fatalf("Unable to read index.html: %v", err) - } - - tmpl, err := template.New("index").Parse(string(b)) - if err != nil { - logging.Logger().Fatalf("Unable to parse index.html template: %v", err) - } - - return func(ctx *fasthttp.RequestCtx) { - nonce := utils.RandomString(32, alphaNumericRunes) - - ctx.SetContentType("text/html; charset=utf-8") - - if os.Getenv("ENVIRONMENT") == dev { - ctx.Response.Header.Add("Content-Security-Policy", fmt.Sprintf("default-src 'self' 'unsafe-eval'; object-src 'none'; style-src 'self' 'nonce-%s'", nonce)) - } else { - ctx.Response.Header.Add("Content-Security-Policy", fmt.Sprintf("default-src 'self'; object-src 'none'; style-src 'self' 'nonce-%s'", nonce)) - } - - err := tmpl.Execute(ctx.Response.BodyWriter(), struct{ Base, CSPNonce, RememberMe, ResetPassword string }{Base: base, CSPNonce: nonce, RememberMe: rememberMe, ResetPassword: resetPassword}) - if err != nil { - ctx.Error("An error occurred", 503) - logging.Logger().Errorf("Unable to execute template: %v", err) - - return - } - } -} diff --git a/internal/server/public_html.gen.go b/internal/server/public_html.gen.go index 509991096..518dbf23b 100644 --- a/internal/server/public_html.gen.go +++ b/internal/server/public_html.gen.go @@ -4,4 +4,4 @@ import "aletheia.icu/broccoli/fs" // Mock the embedded filesystem for unit tests. The bundle is built from an empty file and // allows to run the dev workflow without failure. -var br = fs.New(false, []byte("\x1b~\x00\x80\x8d\x94n\xc2|\x84J\xf7\xbfn\xfd\xf7w;.\x8d m\xb2&\xd1Z\xec\xb2\x05\xb9\xc00\x8a\xf7(\x80^78\t(\f\f\xc3p\xc2\xc1\x06[a\xa2\xb3\xa4P\xe5\xa14\xfb\x19\xb2cp\xf6\x90-Z\xb2\x11\xe0l\xa1\x80\\\x95Vh\t\xc5\x06\x16\xfa\x8c\xc0\"!\xa5\xcf\xf7$\x9a\xb2\a`\xc6\x18\xc8~\xce8\r\x16Z\x9d\xc3\xe3\xff\x00")) +var br = fs.New(false, []byte("\x1b\xf7\x00\x00ħ?\xf5\xbd\xaci\x936'\x9e\x8b\xe5*\xda\xfbֵ@6\x96\xa0\"e\xc9xz\x92eaH)\aA\x18a`m\xcd#\xfd\xc1\xbe\x1d\xe4h\x87:\xd9h/2~\x17\x92'w~J\x94\xe6\x178?\x80n\xbe˔\xea@\x95J/n\x82V\xfa\x02\x1etB\x81\xa0t\xa2·\xf5\xfe\x02̿\xf9\x05E\xb2Q\xcb\xe5\xea\xb6\xdfQ\xdfS\n\x0e\xff蓼\xe4\xefR-Nkʍ\x1d\xed\xd5 [&*\x0f\f\x83\xd6\xec\x92\v\x1b\x19\xb4\x1d\x91\x00")) diff --git a/internal/server/server.go b/internal/server/server.go index 6c75a423e..4168acadd 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -25,22 +25,28 @@ import ( // StartServer start Authelia server with the given configuration and providers. func StartServer(configuration schema.Configuration, providers middlewares.Providers) { autheliaMiddleware := middlewares.AutheliaMiddleware(configuration, providers) - embeddedAssets := "/public_html" + embeddedAssets := "/public_html/" + swaggerAssets := embeddedAssets + "api/" rememberMe := strconv.FormatBool(configuration.Session.RememberMeDuration != "0") resetPassword := strconv.FormatBool(!configuration.AuthenticationBackend.DisableResetPassword) rootFiles := []string{"favicon.ico", "manifest.json", "robots.txt"} - serveIndexHandler := ServeIndex(embeddedAssets, configuration.Server.Path, rememberMe, resetPassword) + serveIndexHandler := ServeTemplatedFile(embeddedAssets, indexFile, configuration.Server.Path, configuration.Session.Name, rememberMe, resetPassword) + serveSwaggerHandler := ServeTemplatedFile(swaggerAssets, indexFile, configuration.Server.Path, configuration.Session.Name, rememberMe, resetPassword) + serveSwaggerAPIHandler := ServeTemplatedFile(swaggerAssets, apiFile, configuration.Server.Path, configuration.Session.Name, rememberMe, resetPassword) r := router.New() r.GET("/", serveIndexHandler) + r.GET("/api/", serveSwaggerHandler) + r.GET("/api/"+apiFile, serveSwaggerAPIHandler) for _, f := range rootFiles { r.GET("/"+f, fasthttpadaptor.NewFastHTTPHandler(br.Serve(embeddedAssets))) } r.GET("/static/{filepath:*}", fasthttpadaptor.NewFastHTTPHandler(br.Serve(embeddedAssets))) + r.GET("/api/{filepath:*}", fasthttpadaptor.NewFastHTTPHandler(br.Serve(embeddedAssets))) r.GET("/api/health", autheliaMiddleware(handlers.HealthGet)) r.GET("/api/state", autheliaMiddleware(handlers.StateGet)) diff --git a/internal/server/template.go b/internal/server/template.go new file mode 100644 index 000000000..209727b53 --- /dev/null +++ b/internal/server/template.go @@ -0,0 +1,65 @@ +package server + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "text/template" + + "github.com/valyala/fasthttp" + + "github.com/authelia/authelia/internal/logging" + "github.com/authelia/authelia/internal/utils" +) + +var alphaNumericRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + +// ServeTemplatedFile serves a templated version of a specified file, +// this is utilised to pass information between the backend and frontend +// and generate a nonce to support a restrictive CSP while using material-ui. +//go:generate broccoli -src ../../public_html -o public_html +func ServeTemplatedFile(publicDir, file, base, session, rememberMe, resetPassword string) fasthttp.RequestHandler { + f, err := br.Open(publicDir + file) + if err != nil { + logging.Logger().Fatalf("Unable to open %s: %s", file, err) + } + + b, err := ioutil.ReadAll(f) + if err != nil { + logging.Logger().Fatalf("Unable to read %s: %s", file, err) + } + + tmpl, err := template.New("file").Parse(string(b)) + if err != nil { + logging.Logger().Fatalf("Unable to parse %s template: %s", file, err) + } + + return func(ctx *fasthttp.RequestCtx) { + nonce := utils.RandomString(32, alphaNumericRunes) + + switch extension := filepath.Ext(file); extension { + case ".html": + ctx.SetContentType("text/html; charset=utf-8") + default: + ctx.SetContentType("text/plain; charset=utf-8") + } + + switch { + case os.Getenv("ENVIRONMENT") == dev: + ctx.Response.Header.Add("Content-Security-Policy", fmt.Sprintf("default-src 'self' 'unsafe-eval'; object-src 'none'; style-src 'self' 'nonce-%s'", nonce)) + case publicDir == "/public_html/api/": + ctx.Response.Header.Add("Content-Security-Policy", fmt.Sprintf("base-uri 'self' ; default-src 'self' ; img-src 'self' https://validator.swagger.io data: ; object-src 'none' ; script-src 'self' 'unsafe-inline' 'nonce-%s' ; style-src 'self' 'nonce-%s'", nonce, nonce)) + default: + ctx.Response.Header.Add("Content-Security-Policy", fmt.Sprintf("default-src 'self' ; object-src 'none'; style-src 'self' 'nonce-%s'", nonce)) + } + + err := tmpl.Execute(ctx.Response.BodyWriter(), struct{ Base, CSPNonce, Session, RememberMe, ResetPassword string }{Base: base, CSPNonce: nonce, Session: session, RememberMe: rememberMe, ResetPassword: resetPassword}) + if err != nil { + ctx.Error("An error occurred", 503) + logging.Logger().Errorf("Unable to execute template: %v", err) + + return + } + } +} diff --git a/web/.gitignore b/web/.gitignore index 4d29575de..00ec607c8 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -17,6 +17,7 @@ .env.development.local .env.test.local .env.production.local +.eslintcache npm-debug.log* yarn-debug.log*