diff --git a/.appveyor/appveyor.yml b/.appveyor/appveyor.yml index 50849831f..391c75eaf 100644 --- a/.appveyor/appveyor.yml +++ b/.appveyor/appveyor.yml @@ -7,8 +7,8 @@ environment: secure: 3kWTz99Qj+ipyaR73CxcJeGRRbmk84MF2ERDu6MyY10cjHAi6s3AVZ2Ccoa+Ioyt appName: saml2aws install: -- set PATH=C:\msys64\mingw64\bin;C:\go118\bin;%PATH% -- set GOROOT=C:\go118 +- set PATH=C:\msys64\mingw64\bin;C:\go120\bin;%PATH% +- set GOROOT=C:\go120 - ps: >- $VerbosePreference = 'Continue' diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..6162e9cc4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +.buildtemp \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1bb13ea22..1e0337155 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,3 +6,9 @@ updates: interval: "weekly" labels: - "type: dependencies" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + labels: + - "type: dependencies" diff --git a/.github/win-msi/out/.gitignore b/.github/win-msi/out/.gitignore new file mode 100644 index 000000000..1287e9bd7 --- /dev/null +++ b/.github/win-msi/out/.gitignore @@ -0,0 +1,2 @@ +** +!.gitignore diff --git a/.github/win-msi/src/saml2aws.wxs b/.github/win-msi/src/saml2aws.wxs new file mode 100644 index 000000000..f1b2c8571 --- /dev/null +++ b/.github/win-msi/src/saml2aws.wxs @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.github/win-msi/wix.sh b/.github/win-msi/wix.sh new file mode 100644 index 000000000..cbf8dc092 --- /dev/null +++ b/.github/win-msi/wix.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +candle src/saml2aws.wxs -dSaml2AwsVer=${VERSION} -o "out/" +light -sval "out/saml2aws.wixobj" -o "out/saml2aws_${VERSION}_windows_amd64.msi" diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 024ac5ec9..e7b2891b9 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -2,12 +2,11 @@ name: Go on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] jobs: - build: name: Build runs-on: ${{ matrix.os }} @@ -15,54 +14,97 @@ jobs: matrix: os: [ubuntu-latest, macOS-latest, macos-11] steps: + - name: Set up Go 1.x + uses: actions/setup-go@v4 + with: + go-version: 1.20.x - - name: Set up Go 1.x - uses: actions/setup-go@v2 - with: - go-version: 1.18.x + - name: Check out code into the Go module directory + uses: actions/checkout@v3 - - name: Check out code into the Go module directory - uses: actions/checkout@v2 + - name: Test + run: | + go test -v ./... -coverprofile=${{ matrix.os }}_coverage.txt -covermode=atomic - - name: Test - run: go test -v ./... + - name: Upload coverage report + uses: actions/upload-artifact@v3 + with: + name: reports + path: ${{ matrix.os }}_coverage.txt + if-no-files-found: error + retention-days: 1 - - name: Install - run: go install ./cmd/saml2aws + - name: Install + run: go install ./cmd/saml2aws linting: name: lint runs-on: ubuntu-latest steps: + - name: Set up Go 1.x + uses: actions/setup-go@v4 + with: + go-version: 1.20.x - - name: Set up Go 1.x - uses: actions/setup-go@v2 - with: - go-version: 1.18.x + - name: Check out code into the Go module directory + uses: actions/checkout@v3 - - name: Check out code into the Go module directory - uses: actions/checkout@v2 + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: v1.53.2 + args: --timeout=2m - - name: golangci-lint - uses: golangci/golangci-lint-action@v2 - with: - version: v1.45.2 + coverage: + name: coverage + permissions: + contents: read + runs-on: ubuntu-latest + needs: [build] + steps: + - uses: actions/checkout@v3 + - name: Download coverage reports + uses: actions/download-artifact@v3 + with: + name: reports + path: reports + + - name: Codecov + uses: codecov/codecov-action@v3 + with: + directory: reports + flags: unittests release-build: name: release-build - runs-on: ubuntu-latest + strategy: + matrix: + os: + - ubuntu-latest + - ubuntu-20.04 + - macos-latest + runs-on: ${{ matrix.os }} steps: + - name: Set up Go 1.x + uses: actions/setup-go@v4 + with: + go-version: 1.20.x + + - name: Check out code into the Go module directory + uses: actions/checkout@v3 - - name: Set up Go 1.x - uses: actions/setup-go@v2 - with: - go-version: 1.18.x + - name: Install dependency required for linux builds + if: matrix.os == 'ubuntu-20.04' + run: sudo apt-get update && sudo apt-get install -y libudev-dev - - name: Check out code into the Go module directory - uses: actions/checkout@v2 + - name: GoReleaser + uses: goreleaser/goreleaser-action@v4 + with: + version: latest + args: build --snapshot --clean --config .goreleaser.${{ matrix.os }}.yml - - name: GoReleaser - uses: goreleaser/goreleaser-action@v2 - with: - version: latest - args: build --snapshot --rm-dist + - name: Upload + uses: actions/upload-artifact@v3 + with: + name: saml2aws + path: dist/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be7f776f2..a8c25e48f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,25 +4,113 @@ on: push: tags: - '*' + workflow_dispatch: + inputs: + tag: + description: The tag to run against. This trigger only runs the MSI builder. + required: true jobs: release: name: release - runs-on: macOS-latest + strategy: + # the goreleaser and the Github release API doesn't handle concurrent + # access well, so run goreleaser serially + max-parallel: 1 + matrix: + os: + - ubuntu-latest + - ubuntu-20.04 + - macos-latest + runs-on: ${{ matrix.os }} + if: github.event_name != 'workflow_dispatch' + permissions: write-all steps: - name: Set up Go 1.x - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: - go-version: 1.18.x + go-version: 1.20.x - name: Check out code into the Go module directory - uses: actions/checkout@v2 + uses: actions/checkout@v3 + - name: Install dependency required for linux builds + if: matrix.os == 'ubuntu-20.04' + run: sudo apt-get update && sudo apt-get install -y libudev-dev + + - name: Add Lowercase Repository Name to Environment + run: | + echo REPOSITORY_NAME_LOWERCASE=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV + + - uses: "docker/login-action@v2" + if: matrix.os == 'ubuntu-20.04' + with: + registry: "ghcr.io" + username: "${{ github.actor }}" + password: "${{ secrets.GITHUB_TOKEN }}" - name: GoReleaser - uses: goreleaser/goreleaser-action@v2 + uses: goreleaser/goreleaser-action@v4 with: version: latest - args: release --rm-dist + args: release --clean --config .goreleaser.${{ matrix.os }}.yml env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + IMAGE_NAME: ${{ env.REPOSITORY_NAME_LOWERCASE }} + + windows-msi: + name: Build Windows MSI and upload to release + runs-on: ubuntu-latest + permissions: + contents: write + needs: [release] + if: >- # https://github.com/actions/runner/issues/491 + always() && + (needs.release.result == 'success' || needs.release.result == 'skipped') + env: + INSTALLER: ${{ github.workspace }}/.github/win-msi + BIN: ${{ github.workspace }}/.github/win-msi/src/bin + WIXIMG: dactiv/wix@sha256:17d232708589641f5632f9a1ff9463ad087b192cea7b8e6012d2b47ec6af5f6c + steps: + - name: Normalize tag values + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]] ; then + VER=${{ github.event.inputs.tag }} + else + VER=${GITHUB_REF/refs\/tags\//} + fi + + VERSION=${VER//v} + + echo "VER_TAG=$VER" >> $GITHUB_ENV + echo "VERSION=$VERSION" >> $GITHUB_ENV + echo "ASSET=saml2aws_${VERSION}_windows_amd64.zip" >> $GITHUB_ENV + + - name: Check out code + uses: actions/checkout@v3 + + - name: Retrieve the release asset + id: asset + uses: robinraju/release-downloader@efa4cd07bd0195e6cc65e9e30c251b49ce4d3e51 # v1.8 + with: + repository: ${{ github.repository }} + tag: ${{ env.VER_TAG }} + fileName: ${{ env.ASSET }} + out-file-path: ${{ env.BIN }} + + - name: Unzip asset + working-directory: ${{ env.BIN }} + run: unzip "${ASSET}" + + - name: Build MSI + run: | + # container does not run as root + chmod -R o+rw "${INSTALLER}" + + cat "${INSTALLER}/wix.sh" | docker run --rm -i -e VERSION -v "${INSTALLER}:/wix" ${WIXIMG} /bin/sh + + - name: Upload the asset to the release + uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15 / v1 + with: + tag_name: ${{ env.VER_TAG }} + files: ${{ env.INSTALLER }}/out/*.msi diff --git a/.gitignore b/.gitignore index 186524f9f..dab5a8422 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ vendor /package /stage coverage.txt +coverage.xml .ctags .vscode bin/ @@ -19,3 +20,4 @@ bin/ # direnv .envrc +.buildtemp \ No newline at end of file diff --git a/.golangci.yaml b/.golangci.yaml index 8cd3c6a64..2d5a1d26d 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -2,13 +2,10 @@ linters: disable-all: true enable: - goimports - - deadcode - errcheck - gosimple - govet - ineffassign - staticcheck - - structcheck - typecheck - unused - - varcheck diff --git a/.goreleaser.yml b/.goreleaser.macos-latest.yml similarity index 82% rename from .goreleaser.yml rename to .goreleaser.macos-latest.yml index 393f7c395..de65cbc4a 100644 --- a/.goreleaser.yml +++ b/.goreleaser.macos-latest.yml @@ -10,9 +10,7 @@ builds: ldflags: - -s -w -X main.Version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} goos: - - windows - darwin - - linux goarch: - amd64 - arm64 @@ -20,10 +18,9 @@ builds: archives: - format: tar.gz wrap_in_directory: false - format_overrides: - - goos: windows - format: zip # remove README and LICENSE files: - LICENSE.md - README.md +checksum: + name_template: "{{ .ProjectName }}_{{ .Version }}_darwin_checksums.txt" diff --git a/.goreleaser.ubuntu-20.04.yml b/.goreleaser.ubuntu-20.04.yml new file mode 100644 index 000000000..497fe4787 --- /dev/null +++ b/.goreleaser.ubuntu-20.04.yml @@ -0,0 +1,94 @@ +--- +project_name: saml2aws-u2f + +builds: +- id: saml2aws + main: ./cmd/saml2aws/main.go + binary: saml2aws + flags: + - -trimpath + - -v + ldflags: + - -s -w -X main.Version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} + goos: + - linux + goarch: + - amd64 + overrides: + - goos: linux + goarch: amd64 + goamd64: v1 + tags: + - hidraw + env: + - CGO_ENABLED=1 +- id: saml2aws-static + main: ./cmd/saml2aws/main.go + binary: saml2aws + flags: + - -trimpath + - -v + ldflags: + - -s -w -X main.Version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -extldflags "-static" + goos: + - linux + goarch: + - amd64 + - arm64 + - arm + env: + - CGO_ENABLED=0 +archives: + - id: saml2aws + format: tar.gz + builds: [saml2aws] + wrap_in_directory: false + # remove README and LICENSE + files: + - LICENSE.md + - README.md + - id: saml2aws-static + format: tar.gz + builds: [saml2aws-static] + wrap_in_directory: false + # remove README and LICENSE + files: + - LICENSE.md + - README.md + name_template: "{{ .ProjectName }}_static_{{ .Version }}_{{ .Os }}_{{ .Arch }}" +checksum: + name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.txt" +dockers: + - id: amd64 + goos: linux + goarch: amd64 + use: buildx + ids: + - saml2aws-static + image_templates: + - ghcr.io/{{ .Env.IMAGE_NAME }}:{{ .Version }}-amd64 + - ghcr.io/{{ .Env.IMAGE_NAME }}:latest-amd64 + build_flag_templates: + - "--build-arg=BASE_IMAGE_ARCH=static-debian11" + - "--platform=linux/amd64" + - id: arm64 + goos: linux + goarch: arm64 + use: buildx + ids: + - saml2aws-static + image_templates: + - ghcr.io/{{ .Env.IMAGE_NAME }}:{{ .Version }}-arm64 + - ghcr.io/{{ .Env.IMAGE_NAME }}:latest-arm64 + build_flag_templates: + - "--build-arg=BASE_IMAGE_ARCH=static:latest-arm64" + - "--platform=linux/arm64" +docker_manifests: + - name_template: ghcr.io/{{ .Env.IMAGE_NAME }}:{{ .Version }} + image_templates: + - ghcr.io/{{ .Env.IMAGE_NAME }}:{{ .Version }}-amd64 + - ghcr.io/{{ .Env.IMAGE_NAME }}:{{ .Version }}-arm64 + - name_template: ghcr.io/{{ .Env.IMAGE_NAME }}:latest + image_templates: + - ghcr.io/{{ .Env.IMAGE_NAME }}:latest-amd64 + - ghcr.io/{{ .Env.IMAGE_NAME }}:latest-arm64 \ No newline at end of file diff --git a/.goreleaser.ubuntu-latest.yml b/.goreleaser.ubuntu-latest.yml new file mode 100644 index 000000000..5db45000c --- /dev/null +++ b/.goreleaser.ubuntu-latest.yml @@ -0,0 +1,36 @@ +--- +project_name: saml2aws + +builds: +- main: ./cmd/saml2aws/main.go + binary: saml2aws + flags: + - -trimpath + - -v + ldflags: + - -s -w -X main.Version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} + goos: + - windows + - linux + goarch: + - amd64 + - arm64 + - arm + overrides: + - goos: linux + goarch: amd64 + goamd64: v1 + env: + - CGO_ENABLED=0 +archives: + - format: tar.gz + wrap_in_directory: false + format_overrides: + - goos: windows + format: zip + # remove README and LICENSE + files: + - LICENSE.md + - README.md +checksum: + name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.txt" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..5a030a50e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,4 @@ +ARG BASE_IMAGE_ARCH=static-debian11 +FROM gcr.io/distroless/$BASE_IMAGE_ARCH +COPY saml2aws / +ENTRYPOINT ["/saml2aws"] \ No newline at end of file diff --git a/Dockerfile.build b/Dockerfile.build new file mode 100644 index 000000000..5d70bc62f --- /dev/null +++ b/Dockerfile.build @@ -0,0 +1,37 @@ +# base image +FROM ubuntu:jammy +ENV TZ=Australia/Sydney +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +# add arm64 architecture +RUN apt-get update +RUN dpkg --add-architecture arm64 + +## arch-qualify the current repositories +RUN sed -i "s/deb h/deb [arch=amd64] h/g" /etc/apt/sources.list + +## add arm64's repos +RUN echo "# arm64 repositories" >> /etc/apt/sources.list +RUN echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy main restricted" >> /etc/apt/sources.list +RUN echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main restricted" >> /etc/apt/sources.list +RUN echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy universe" >> /etc/apt/sources.list +RUN echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates universe" >> /etc/apt/sources.list +RUN echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy multiverse" >> /etc/apt/sources.list +RUN echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates multiverse" >> /etc/apt/sources.list +RUN echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-backports main restricted universe multiverse" >> /etc/apt/sources.list +RUN echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main restricted" >> /etc/apt/sources.list +RUN echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security universe" >> /etc/apt/sources.list +RUN echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security multiverse" >> /etc/apt/sources.list + +RUN apt-get update && apt-get install -y build-essential git ca-certificates golang libudev-dev curl gnupg lsb-release curl gcc-arm* binutils-arm-linux-gnueabi crossbuild-essential-arm64 wget + +RUN echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \ + $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null +RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg +RUN apt-get update && apt-get install -y docker-ce-cli +# Replicate install of the same version of Golang that we are using in Github actions +RUN wget https://go.dev/dl/go1.20.2.linux-amd64.tar.gz && tar -C /usr/local -xzf go1.20.2.linux-amd64.tar.gz && rm go1.20.2.linux-amd64.tar.gz +RUN go install github.com/goreleaser/goreleaser@latest +ENV GOROOT="/usr/local/go" +ENV PATH="/root/go/bin:$GOROOT/bin:${PATH}" diff --git a/Makefile b/Makefile index 3dac85ed8..1db3490e5 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,10 @@ NAME=saml2aws ARCH=$(shell uname -m) -VERSION=2.28.0 +OS?=$(shell uname) ITERATION := 1 GOLANGCI_VERSION = 1.45.2 -GORELEASER_VERSION = 0.157.0 +GORELEASER := $(shell command -v goreleaser 2> /dev/null) SOURCE_FILES?=$$(go list ./... | grep -v /vendor/) TEST_PATTERN?=. @@ -12,6 +12,15 @@ TEST_OPTIONS?= BIN_DIR := $(CURDIR)/bin +# Choose the right config file for the OS +ifeq ($(OS),Darwin) + CONFIG_FILE?=$(CURDIR)/.goreleaser.macos-latest.yml +else ifeq ($(OS),Linux) + CONFIG_FILE?=$(CURDIR)/.goreleaser.ubuntu-20.04.yml +else + $(error Unsupported build OS: $(OS)) +endif + ci: prepare test mod: @@ -35,10 +44,18 @@ install: go install ./cmd/saml2aws .PHONY: mod -build: $(BIN_DIR)/goreleaser - $(BIN_DIR)/goreleaser build --snapshot --rm-dist +build: + +ifndef GORELEASER + $(error "goreleaser is not available please install and ensure it is on PATH") +endif + goreleaser build --snapshot --clean --config $(CONFIG_FILE) .PHONY: build +release-local: $(BIN_DIR)/goreleaser + goreleaser release --snapshot --rm-dist --config $(CONFIG_FILE) +.PHONY: release-local + clean: @rm -fr ./build .PHONY: clean @@ -52,3 +69,15 @@ test: @echo "--- test all the things" @go test -cover ./... .PHONY: test + +# It can be difficult to set up and test everything locally. Using this target you can build and run a docker container +# that has all the tools you need to build and test saml2aws. This is particularly useful on Mac as it allows the Linux +# and Docker builds to be tested. +# Note: By necessity, this target mounts the Docker socket into the container. This is a security risk and should not +# be used on a production system. +# Note: Files written by the container will be owned by root. This is a limitation of the Docker socket mount. +# You may need to run `docker run --privileged --rm tonistiigi/binfmt --install all` to enable the buildx plugin. +docker-build-environment: + docker build --platform=amd64 -t saml2aws/build -f Dockerfile.build . + docker run -it --rm -v /var/run/docker.sock:/var/run/docker.sock -e BUILDX_CONFIG=$(PWD)/.buildtemp -e GOPATH=$(PWD)/.buildtemp -e GOTMPDIR=$(PWD)/.buildtemp -e GOCACHE=$(PWD)/.buildtemp/.cache -e GOENV=$(PWD)/.buildtemp/env -v $(PWD):$(PWD) -w $(PWD) saml2aws/build:latest +.PHONY: docker-build-environment \ No newline at end of file diff --git a/README.md b/README.md index db2717cb6..2888cfd45 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ -# saml2aws [![GitHub Actions status](https://github.com/Versent/saml2aws/workflows/Go/badge.svg?branch=master)](https://github.com/Versent/saml2aws/actions?query=workflow%3AGo) [![Build status - Windows](https://ci.appveyor.com/api/projects/status/ptpi18kci16o4i82/branch/master?svg=true)](https://ci.appveyor.com/project/davidobrien1985/saml2aws/branch/master) +# saml2aws + +[![GitHub Actions status](https://github.com/Versent/saml2aws/workflows/Go/badge.svg?branch=master)](https://github.com/Versent/saml2aws/actions?query=workflow%3AGo) [![Build status - Windows](https://ci.appveyor.com/api/projects/status/ptpi18kci16o4i82/branch/master?svg=true)](https://ci.appveyor.com/project/davidobrien1985/saml2aws/branch/master) +[![codecov](https://codecov.io/gh/Versent/saml2aws/branch/master/graph/badge.svg)](https://codecov.io/gh/Versent/saml2aws) CLI tool which enables you to login and retrieve [AWS](https://aws.amazon.com/) temporary credentials using with [ADFS](https://msdn.microsoft.com/en-us/library/bb897402.aspx) or [PingFederate](https://www.pingidentity.com/en/products/pingfederate.html) Identity Providers. @@ -18,25 +21,47 @@ The process goes something like this: ## Table of Contents -- [Table of Contents](#table-of-contents) -- [Requirements](#requirements) -- [Caveats](#caveats) -- [Install](#install) +- [saml2aws](#saml2aws) + - [Table of Contents](#table-of-contents) + - [Requirements](#requirements) + - [Caveats](#caveats) + - [Install](#install) - [OSX](#osx) - [Windows](#windows) - [Linux](#linux) -- [Autocomplete](#autocomplete) -- [Dependency Setup](#dependency-setup) -- [Usage](#usage) + - [Using Make](#using-make) + - [Arch Linux and its derivatives](#arch-linux-and-its-derivatives) + - [Void Linux](#void-linux) + - [Autocomplete](#autocomplete) + - [Bash](#bash) + - [Zsh](#zsh) + - [Dependency Setup](#dependency-setup) + - [Usage](#usage) - [`saml2aws script`](#saml2aws-script) + - [`saml2aws exec`](#saml2aws-exec) - [Configuring IDP Accounts](#configuring-idp-accounts) -- [Example](#example) -- [Advanced Configuration](#advanced-configuration) - - [Dev Account Setup](#dev-account-setup) - - [Test Account Setup](#test-account-setup) -- [Building](#building) -- [Environment vars](#environment-vars) -- [Provider Specific Documentation](#provider-specific-documentation) + - [Example](#example) + - [Advanced Configuration](#advanced-configuration) + - [Windows Subsystem Linux (WSL) Configuration](#windows-subsystem-linux-wsl-configuration) + - [Option 1: Disable Keychain](#option-1-disable-keychain) + - [Option 2: Configure Pass to be the default keyring](#option-2-configure-pass-to-be-the-default-keyring) + - [Configuring Multiple Accounts](#configuring-multiple-accounts) + - [Dev Account Setup](#dev-account-setup) + - [Test Account Setup](#test-account-setup) + - [Advanced Configuration (Multiple AWS account access but SAML authenticate against a single 'SSO' AWS account)](#advanced-configuration-multiple-aws-account-access-but-saml-authenticate-against-a-single-sso-aws-account) + - [Advanced Configuration - additional parameters](#advanced-configuration---additional-parameters) + - [Building](#building) + - [macOS](#macos) + - [Linux](#linux-1) + - [Environment vars](#environment-vars) + - [Provider Specific Documentation](#provider-specific-documentation) +- [Dependencies](#dependencies) +- [Releasing](#releasing) +- [Debugging Issues with IDPs](#debugging-issues-with-idps) +- [Using saml2aws as credential process](#using-saml2aws-as-credential-process) +- [Caching the saml2aws SAML assertion for immediate reuse](#caching-the-saml2aws-saml-assertion-for-immediate-reuse) +- [Okta Sessions](#okta-sessions) +- [License](#license) ## Requirements @@ -52,7 +77,7 @@ The process goes something like this: * [Akamai](pkg/provider/akamai/README.md) * OneLogin * NetIQ - * Browser, this uses [playwright-go](github.com/mxschmitt/playwright-go) to run a sandbox chromium window. + * Browser, this uses [playwright-go](github.com/playwright-community/playwright-go) to run a sandbox chromium window. * [Auth0](pkg/provider/auth0/README.md) NOTE: Currently, MFA not supported * AWS SAML Provider configured @@ -89,12 +114,30 @@ saml2aws --version While brew is available for Linux you can also run the following without using a package manager. ``` +mkdir -p ~/.local/bin CURRENT_VERSION=$(curl -Ls https://api.github.com/repos/Versent/saml2aws/releases/latest | grep 'tag_name' | cut -d'v' -f2 | cut -d'"' -f1) -wget -c https://github.com/Versent/saml2aws/releases/download/v${CURRENT_VERSION}/saml2aws_${CURRENT_VERSION}_linux_amd64.tar.gz -O - | tar -xzv -C ~/.local/bin +wget -c "https://github.com/Versent/saml2aws/releases/download/v${CURRENT_VERSION}/saml2aws_${CURRENT_VERSION}_linux_amd64.tar.gz" -O - | tar -xzv -C ~/.local/bin chmod u+x ~/.local/bin/saml2aws hash -r saml2aws --version ``` +If U2F support is required then there are separate builds for this - use the following download URL instead: +``` +wget -c "https://github.com/Versent/saml2aws/releases/download/v${CURRENT_VERSION}/saml2aws-u2f_${CURRENT_VERSION}_linux_amd64.tar.gz" -O - | tar -xzv -C ~/.local/bin +``` + +#### Using Make + +You will need [Go Tools](https://golang.org/doc/install) (you can check your package maintainer as well) installed and the [Go Lint tool](https://github.com/alecthomas/gometalinter) + +Clone this repo to your `$GOPATH/src` directory + +Now you can install by running + +``` +make +make install +``` #### [Arch Linux](https://archlinux.org/) and its derivatives @@ -179,6 +222,8 @@ Commands: --client-secret=CLIENT-SECRET OneLogin client secret, used to generate API access token. (env: ONELOGIN_CLIENT_SECRET) --subdomain=SUBDOMAIN OneLogin subdomain of your company account. (env: ONELOGIN_SUBDOMAIN) + --mfa-ip-address=MFA-IP-ADDRESS + IP address whitelisting defined in OneLogin MFA policies. (env: ONELOGIN_MFA_IP_ADDRESS) -p, --profile=PROFILE The AWS profile to save the temporary credentials. (env: SAML2AWS_PROFILE) --resource-id=RESOURCE-ID F5APM SAML resource ID of your company account. (env: SAML2AWS_F5APM_RESOURCE_ID) --config=CONFIG Path/filename of saml2aws config file (env: SAML2AWS_CONFIGFILE) @@ -196,12 +241,15 @@ Commands: --client-id=CLIENT-ID OneLogin client id, used to generate API access token. (env: ONELOGIN_CLIENT_ID) --client-secret=CLIENT-SECRET OneLogin client secret, used to generate API access token. (env: ONELOGIN_CLIENT_SECRET) + --mfa-ip-address=MFA-IP-ADDRESS + IP address whitelisting defined in OneLogin MFA policies. (env: ONELOGIN_MFA_IP_ADDRESS) --force Refresh credentials even if not expired. --credential-process Enables AWS Credential Process support by outputting credentials to STDOUT in a JSON message. --credentials-file=CREDENTIALS-FILE The file that will cache the credentials retrieved from AWS. When not specified, will use the default AWS credentials file location. (env: SAML2AWS_CREDENTIALS_FILE) --cache-saml Caches the SAML response (env: SAML2AWS_CACHE_SAML) --cache-file=CACHE-FILE The location of the SAML cache file (env: SAML2AWS_SAML_CACHE_FILE) + --download-browser-driver Automatically download browsers for Browser IDP. (env: SAML2AWS_AUTO_BROWSER_DOWNLOAD) --disable-sessions Do not use Okta sessions. Uses Okta sessions by default. (env: SAML2AWS_OKTA_DISABLE_SESSIONS) --disable-remember-device Do not remember Okta MFA device. Remembers MFA device by default. (env: SAML2AWS_OKTA_DISABLE_REMEMBER_DEVICE) @@ -235,7 +283,7 @@ Commands: Emit a script that will export environment variables. -p, --profile=PROFILE The AWS profile to save the temporary credentials. (env: SAML2AWS_PROFILE) - --shell=bash Type of shell environment. Options include: bash, powershell, fish, env + --shell=bash Type of shell environment. Options include: bash, /bin/sh, powershell, fish, env --credentials-file=CREDENTIALS-FILE The file that will cache the credentials retrieved from AWS. When not specified, will use the default AWS credentials file location. (env: SAML2AWS_CREDENTIALS_FILE) @@ -255,7 +303,7 @@ export AWS_CREDENTIAL_EXPIRATION="2016-09-04T38:27:00Z00:00" SAML2AWS_PROFILE=saml ``` -Powershell, and fish shells are supported as well. +Powershell, sh and fish shells are supported as well. Env is useful for all AWS SDK compatible tools that can source an env file. It is a powerful combo with docker and the `--env-file` parameter. If you use `eval $(saml2aws script)` frequently, you may want to create a alias for it: @@ -498,6 +546,18 @@ region = us-east-1 To use this you will need to export `AWS_DEFAULT_PROFILE=customer-test` environment variable to target `test`. +### Playwright Browser Drivers for Browser IDP + +If you are using the Browser Identity Provider, on first invocation of `saml2aws login` you need to remember to install +the browser drivers in order for playwright-go to work. Otherwise you will see the following error message: + +`Error authenticating to IDP.: could not start driver: fork/exec ... no such file or directory` + +To install the drivers, you can: +* Pass `--download-browser-driver` to `saml2aws login` +* Set in your shell environment `SAML2AWS_AUTO_BROWSER_DOWNLOAD=true` +* Set `download_browser_driver = true` in your saml2aws config file, i.e. `~/.saml2aws` + ## Advanced Configuration (Multiple AWS account access but SAML authenticate against a single 'SSO' AWS account) Example: @@ -629,6 +689,8 @@ region = us-east-1 ``` ## Building +### macOS + To build this software on osx clone to the repo to `$GOPATH/src/github.com/versent/saml2aws` and ensure you have `$GOPATH/bin` in your `$PATH`. ``` @@ -653,6 +715,26 @@ Before raising a PR please run the linter. make lint-fix ``` +### Linux + +To build this software on Debian/Ubuntu, you need to install a build dependency: + +``` +sudo apt install libudev-dev +``` + +You also need [GoReleaser](https://github.com/goreleaser/goreleaser) installed, and the binary (or a symlink) in `bin/goreleaser`. + +``` +ln -s $(command -v goreleaser) bin/goreleaser +``` + +Then you can build: + +``` +make build +``` + ## Environment vars The exec sub command will export the following environment variables. diff --git a/aws_account.go b/aws_account.go index bdf7e83d6..ff28c3abf 100644 --- a/aws_account.go +++ b/aws_account.go @@ -3,7 +3,7 @@ package saml2aws import ( "bytes" "fmt" - "io/ioutil" + "io" "net/http" "net/url" @@ -24,7 +24,7 @@ func ParseAWSAccounts(audience string, samlAssertion string) ([]*AWSAccount, err return nil, errors.Wrap(err, "error retrieving AWS login form") } - data, err := ioutil.ReadAll(res.Body) + data, err := io.ReadAll(res.Body) if err != nil { return nil, errors.Wrap(err, "error retrieving AWS login body") } diff --git a/aws_account_test.go b/aws_account_test.go index 9ea4ec11c..b5c86c43b 100644 --- a/aws_account_test.go +++ b/aws_account_test.go @@ -1,14 +1,14 @@ package saml2aws import ( - "io/ioutil" + "os" "testing" "github.com/stretchr/testify/assert" ) func TestExtractAWSAccounts(t *testing.T) { - data, err := ioutil.ReadFile("testdata/saml.html") + data, err := os.ReadFile("testdata/saml.html") assert.Nil(t, err) accounts, err := ExtractAWSAccounts(data) diff --git a/aws_role.go b/aws_role.go index c843f9b8f..60ff2246b 100644 --- a/aws_role.go +++ b/aws_role.go @@ -2,6 +2,7 @@ package saml2aws import ( "fmt" + "regexp" "strings" ) @@ -29,7 +30,8 @@ func ParseAWSRoles(roles []string) ([]*AWSRole, error) { } func parseRole(role string) (*AWSRole, error) { - tokens := strings.Split(role, ",") + r, _ := regexp.Compile("arn:([^:\n]*):([^:\n]*):([^:\n]*):([^:\n]*):(([^:/\n]*)[:/])?([^:,\n]*)") + tokens := r.FindAllString(role, -1) if len(tokens) != 2 { return nil, fmt.Errorf("Invalid role string only %d tokens", len(tokens)) diff --git a/choco/VERIFICATION.txt b/choco/VERIFICATION.txt index 02109c0a6..0b4e283e8 100644 --- a/choco/VERIFICATION.txt +++ b/choco/VERIFICATION.txt @@ -2,7 +2,7 @@ VERIFICATION Verification is intended to assist the Chocolatey moderators and community in verifying that this package's contents are trustworthy. -The installer has been automatically built from source whch can be found on +The installer has been automatically built from source which can be found on and can be verified like this: 1. Download the release version from @@ -11,4 +11,4 @@ and can be verified like this: - Use chocolatey utility 'checksum.exe' Compare the checksums there with the checksum of the local binary in C:\ProgramData\Chocolatey\lib\saml2aws\src\saml2aws.exe -File 'LICENSE.txt' is obtained from . \ No newline at end of file +File 'LICENSE.txt' is obtained from . diff --git a/cmd/saml2aws/commands/console.go b/cmd/saml2aws/commands/console.go index cf11bf0d3..70e1f20d6 100644 --- a/cmd/saml2aws/commands/console.go +++ b/cmd/saml2aws/commands/console.go @@ -3,7 +3,7 @@ package commands import ( "encoding/json" "fmt" - "io/ioutil" + "io" "log" "net/http" "net/url" @@ -136,7 +136,7 @@ func federatedLogin(creds *awsconfig.AWSCredentials, consoleFlags *flags.Console } defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return err } diff --git a/cmd/saml2aws/commands/login.go b/cmd/saml2aws/commands/login.go index 86fe3ffd0..0a7766a4e 100644 --- a/cmd/saml2aws/commands/login.go +++ b/cmd/saml2aws/commands/login.go @@ -217,6 +217,19 @@ func resolveLoginDetails(account *cfg.IDPAccount, loginFlags *flags.LoginExecFla loginDetails.ClientSecret = loginFlags.CommonFlags.ClientSecret } + // if you supply an mfa_ip_address in a flag or an IDP account it takes precedence + if account.MFAIPAddress != "" { + loginDetails.MFAIPAddress = account.MFAIPAddress + } else if loginFlags.CommonFlags.MFAIPAddress != "" { + loginDetails.MFAIPAddress = loginFlags.CommonFlags.MFAIPAddress + } + + if loginFlags.DownloadBrowser { + loginDetails.DownloadBrowser = loginFlags.DownloadBrowser + } else if account.DownloadBrowser { + loginDetails.DownloadBrowser = account.DownloadBrowser + } + // log.Printf("loginDetails %+v", loginDetails) // if skip prompt was passed just pass back the flag values diff --git a/cmd/saml2aws/commands/login_darwin.go b/cmd/saml2aws/commands/login_darwin.go index 14b4d9bbe..c2d036354 100644 --- a/cmd/saml2aws/commands/login_darwin.go +++ b/cmd/saml2aws/commands/login_darwin.go @@ -1,3 +1,4 @@ +//go:build darwin && cgo // +build darwin,cgo package commands diff --git a/cmd/saml2aws/commands/login_test.go b/cmd/saml2aws/commands/login_test.go index 0e29f3640..bca0442cf 100644 --- a/cmd/saml2aws/commands/login_test.go +++ b/cmd/saml2aws/commands/login_test.go @@ -15,7 +15,7 @@ import ( func TestResolveLoginDetailsWithFlags(t *testing.T) { - commonFlags := &flags.CommonFlags{URL: "https://id.example.com", Username: "wolfeidau", Password: "testtestlol", MFAToken: "123456", SkipPrompt: true} + commonFlags := &flags.CommonFlags{URL: "https://id.example.com", Username: "wolfeidau", Password: "testtestlol", MFAIPAddress: "127.0.0.1", MFAToken: "123456", SkipPrompt: true} loginFlags := &flags.LoginExecFlags{CommonFlags: commonFlags} idpa := &cfg.IDPAccount{ @@ -27,7 +27,7 @@ func TestResolveLoginDetailsWithFlags(t *testing.T) { loginDetails, err := resolveLoginDetails(idpa, loginFlags) assert.Empty(t, err) - assert.Equal(t, &creds.LoginDetails{Username: "wolfeidau", Password: "testtestlol", URL: "https://id.example.com", MFAToken: "123456"}, loginDetails) + assert.Equal(t, &creds.LoginDetails{Username: "wolfeidau", Password: "testtestlol", URL: "https://id.example.com", MFAToken: "123456", MFAIPAddress: "127.0.0.1"}, loginDetails) } func TestOktaResolveLoginDetailsWithFlags(t *testing.T) { diff --git a/cmd/saml2aws/commands/script.go b/cmd/saml2aws/commands/script.go index 4a736e6b4..0f35b3dbd 100644 --- a/cmd/saml2aws/commands/script.go +++ b/cmd/saml2aws/commands/script.go @@ -20,6 +20,14 @@ export SAML2AWS_PROFILE={{ .ProfileName }} export AWS_CREDENTIAL_EXPIRATION={{ .Expires.Format "2006-01-02T15:04:05Z07:00" }} ` +const shTmpl = `export AWS_ACCESS_KEY_ID={{ .AWSAccessKey }} +export AWS_SECRET_ACCESS_KEY={{ .AWSSecretKey }} +export AWS_SESSION_TOKEN={{ .AWSSessionToken }} +export AWS_SECURITY_TOKEN={{ .AWSSecurityToken }} +export SAML2AWS_PROFILE={{ .ProfileName }} +export AWS_CREDENTIAL_EXPIRATION={{ .Expires.Format "2006-01-02T15:04:05Z07:00" }} +` + const fishTmpl = `set -gx AWS_ACCESS_KEY_ID {{ .AWSAccessKey }} set -gx AWS_SECRET_ACCESS_KEY {{ .AWSSecretKey }} set -gx AWS_SESSION_TOKEN {{ .AWSSessionToken }} @@ -99,6 +107,8 @@ func buildTmpl(shell string, data interface{}) (string, error) { switch shell { case "bash": t, err = t.Parse(bashTmpl) + case "/bin/sh": + t, err = t.Parse(shTmpl) case "powershell": t, err = t.Parse(powershellTmpl) case "fish": diff --git a/cmd/saml2aws/commands/script_test.go b/cmd/saml2aws/commands/script_test.go index 0e44073c4..9d458584f 100644 --- a/cmd/saml2aws/commands/script_test.go +++ b/cmd/saml2aws/commands/script_test.go @@ -25,7 +25,40 @@ func TestBuildTmplBash(t *testing.T) { } st, err := buildTmpl("bash", data) - assert.ErrorIs(t, err, nil) + assert.Nil(t, err) + + expected := []string{ + "export AWS_ACCESS_KEY_ID=access_key", + "export AWS_SECRET_ACCESS_KEY=secret_key", + "export AWS_SESSION_TOKEN=session_token", + "export AWS_SECURITY_TOKEN=security_token", + "export SAML2AWS_PROFILE=test_profile", + } + + for _, test_string := range expected { + assert.Contains(t, st, test_string) + } + +} + +func TestBuildTmplSh(t *testing.T) { + + data := struct { + ProfileName string + *awsconfig.AWSCredentials + }{ + "test_profile", + &awsconfig.AWSCredentials{ + AWSSecretKey: "secret_key", + AWSAccessKey: "access_key", + AWSSessionToken: "session_token", + AWSSecurityToken: "security_token", + Expires: time.Now(), + }, + } + + st, err := buildTmpl("/bin/sh", data) + assert.Nil(t, err) expected := []string{ "export AWS_ACCESS_KEY_ID=access_key", @@ -58,7 +91,7 @@ func TestBuildTmplFish(t *testing.T) { } st, err := buildTmpl("fish", data) - assert.ErrorIs(t, err, nil) + assert.Nil(t, err) expected := []string{ "set -gx AWS_ACCESS_KEY_ID access_key", @@ -91,7 +124,7 @@ func TestBuildTmplEnv(t *testing.T) { } st, err := buildTmpl("env", data) - assert.ErrorIs(t, err, nil) + assert.Nil(t, err) expected := []string{ "AWS_ACCESS_KEY_ID=access_key", diff --git a/cmd/saml2aws/main.go b/cmd/saml2aws/main.go index d6ee181e3..328400df3 100644 --- a/cmd/saml2aws/main.go +++ b/cmd/saml2aws/main.go @@ -2,7 +2,7 @@ package main import ( "crypto/tls" - "io/ioutil" + "io" "log" "net/http" "os" @@ -90,6 +90,7 @@ func main() { cmdConfigure.Flag("client-id", "OneLogin client id, used to generate API access token. (env: ONELOGIN_CLIENT_ID)").Envar("ONELOGIN_CLIENT_ID").StringVar(&commonFlags.ClientID) cmdConfigure.Flag("client-secret", "OneLogin client secret, used to generate API access token. (env: ONELOGIN_CLIENT_SECRET)").Envar("ONELOGIN_CLIENT_SECRET").StringVar(&commonFlags.ClientSecret) cmdConfigure.Flag("subdomain", "OneLogin subdomain of your company account. (env: ONELOGIN_SUBDOMAIN)").Envar("ONELOGIN_SUBDOMAIN").StringVar(&commonFlags.Subdomain) + cmdConfigure.Flag("mfa-ip-address", "IP address whitelisting defined in OneLogin MFA policies. (env: ONELOGIN_MFA_IP_ADDRESS)").Envar("ONELOGIN_MFA_IP_ADDRESS").StringVar(&commonFlags.MFAIPAddress) cmdConfigure.Flag("profile", "The AWS profile to save the temporary credentials. (env: SAML2AWS_PROFILE)").Envar("SAML2AWS_PROFILE").Short('p').StringVar(&commonFlags.Profile) cmdConfigure.Flag("resource-id", "F5APM SAML resource ID of your company account. (env: SAML2AWS_F5APM_RESOURCE_ID)").Envar("SAML2AWS_F5APM_RESOURCE_ID").StringVar(&commonFlags.ResourceID) cmdConfigure.Flag("credentials-file", "The file that will cache the credentials retrieved from AWS. When not specified, will use the default AWS credentials file location. (env: SAML2AWS_CREDENTIALS_FILE)").Envar("SAML2AWS_CREDENTIALS_FILE").StringVar(&commonFlags.CredentialsFile) @@ -107,11 +108,13 @@ func main() { cmdLogin.Flag("duo-mfa-option", "The MFA option you want to use to authenticate with").Envar("SAML2AWS_DUO_MFA_OPTION").EnumVar(&loginFlags.DuoMFAOption, "Passcode", "Phone Call", "Duo Push") cmdLogin.Flag("client-id", "OneLogin client id, used to generate API access token. (env: ONELOGIN_CLIENT_ID)").Envar("ONELOGIN_CLIENT_ID").StringVar(&commonFlags.ClientID) cmdLogin.Flag("client-secret", "OneLogin client secret, used to generate API access token. (env: ONELOGIN_CLIENT_SECRET)").Envar("ONELOGIN_CLIENT_SECRET").StringVar(&commonFlags.ClientSecret) + cmdLogin.Flag("mfa-ip-address", "IP address whitelisting defined in OneLogin MFA policies. (env: ONELOGIN_MFA_IP_ADDRESS)").Envar("ONELOGIN_MFA_IP_ADDRESS").StringVar(&commonFlags.MFAIPAddress) cmdLogin.Flag("force", "Refresh credentials even if not expired.").BoolVar(&loginFlags.Force) cmdLogin.Flag("credential-process", "Enables AWS Credential Process support by outputting credentials to STDOUT in a JSON message.").BoolVar(&loginFlags.CredentialProcess) cmdLogin.Flag("credentials-file", "The file that will cache the credentials retrieved from AWS. When not specified, will use the default AWS credentials file location. (env: SAML2AWS_CREDENTIALS_FILE)").Envar("SAML2AWS_CREDENTIALS_FILE").StringVar(&commonFlags.CredentialsFile) cmdLogin.Flag("cache-saml", "Caches the SAML response (env: SAML2AWS_CACHE_SAML)").Envar("SAML2AWS_CACHE_SAML").BoolVar(&commonFlags.SAMLCache) cmdLogin.Flag("cache-file", "The location of the SAML cache file (env: SAML2AWS_SAML_CACHE_FILE)").Envar("SAML2AWS_SAML_CACHE_FILE").StringVar(&commonFlags.SAMLCacheFile) + cmdLogin.Flag("download-browser-driver", "Automatically download browsers for Browser IDP. (env: SAML2AWS_AUTO_BROWSER_DOWNLOAD)").Envar("SAML2AWS_AUTO_BROWSER_DOWNLOAD").BoolVar(&loginFlags.DownloadBrowser) cmdLogin.Flag("disable-sessions", "Do not use Okta sessions. Uses Okta sessions by default. (env: SAML2AWS_OKTA_DISABLE_SESSIONS)").Envar("SAML2AWS_OKTA_DISABLE_SESSIONS").BoolVar(&commonFlags.DisableSessions) cmdLogin.Flag("disable-remember-device", "Do not remember Okta MFA device. Remembers MFA device by default. (env: SAML2AWS_OKTA_DISABLE_REMEMBER_DEVICE)").Envar("SAML2AWS_OKTA_DISABLE_REMEMBER_DEVICE").BoolVar(&commonFlags.DisableRememberDevice) @@ -150,9 +153,9 @@ func main() { cmdScript.Flag("credentials-file", "The file that will cache the credentials retrieved from AWS. When not specified, will use the default AWS credentials file location. (env: SAML2AWS_CREDENTIALS_FILE)").Envar("SAML2AWS_CREDENTIALS_FILE").StringVar(&commonFlags.CredentialsFile) var shell string cmdScript. - Flag("shell", "Type of shell environment. Options include: bash, powershell, fish, env"). + Flag("shell", "Type of shell environment. Options include: bash, /bin/sh, powershell, fish, env"). Default("bash"). - EnumVar(&shell, "bash", "powershell", "fish", "env") + EnumVar(&shell, "bash", "/bin/sh", "powershell", "fish", "env") // Trigger the parsing of the command line inputs via kingpin command := kingpin.MustParse(app.Parse(os.Args[1:])) @@ -170,8 +173,8 @@ func main() { } if *quiet { - log.SetOutput(ioutil.Discard) - logrus.SetOutput(ioutil.Discard) + log.SetOutput(io.Discard) + logrus.SetOutput(io.Discard) } // Set the default transport settings so all http clients will pick them up. diff --git a/coverage.xml b/coverage.xml deleted file mode 100644 index 3f52527f0..000000000 --- a/coverage.xml +++ /dev/null @@ -1,11135 +0,0 @@ - - - - - /home/markw/Code/notgopath/saml2aws - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/go.mod b/go.mod index 82f225175..ccbfe7b86 100644 --- a/go.mod +++ b/go.mod @@ -1,29 +1,30 @@ module github.com/versent/saml2aws/v2 -go 1.18 +go 1.20 require ( - github.com/99designs/keyring v1.2.1 - github.com/AlecAivazis/survey/v2 v2.3.5 + github.com/99designs/keyring v1.2.2 + github.com/AlecAivazis/survey/v2 v2.3.6 github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e - github.com/PuerkitoBio/goquery v1.8.0 + github.com/PuerkitoBio/goquery v1.8.1 github.com/alecthomas/kingpin v2.2.6+incompatible github.com/avast/retry-go v3.0.0+incompatible - github.com/aws/aws-sdk-go v1.44.59 - github.com/beevik/etree v1.1.0 - github.com/danieljoos/wincred v1.1.2 + github.com/aws/aws-sdk-go v1.44.281 + github.com/beevik/etree v1.2.0 + github.com/danieljoos/wincred v1.2.0 github.com/google/uuid v1.3.0 + github.com/h2non/gock v1.2.0 github.com/keybase/go-keychain v0.0.0-20211119201326-e02f34051621 github.com/marshallbrekka/go-u2fhost v0.0.0-20210111072507-3ccdec8c8105 github.com/mitchellh/go-homedir v1.1.0 - github.com/mxschmitt/playwright-go v0.1400.0 github.com/pkg/errors v0.9.1 - github.com/sirupsen/logrus v1.9.0 + github.com/playwright-community/playwright-go v0.2000.1 + github.com/sirupsen/logrus v1.9.3 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 - github.com/stretchr/testify v1.8.0 - github.com/tidwall/gjson v1.14.1 - golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd - gopkg.in/ini.v1 v1.66.6 + github.com/stretchr/testify v1.8.4 + github.com/tidwall/gjson v1.14.4 + golang.org/x/net v0.10.0 + gopkg.in/ini.v1 v1.67.0 ) require ( @@ -35,9 +36,10 @@ require ( github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dvsekhvalnov/jose2go v1.5.0 // indirect + github.com/go-stack/stack v1.8.1 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect - github.com/gorilla/websocket v1.4.2 // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect + github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/mattn/go-colorable v0.1.2 // indirect @@ -45,13 +47,13 @@ require ( github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/mtibben/percent v0.2.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/objx v0.4.0 // indirect + github.com/stretchr/objx v0.5.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect - golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 // indirect - golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect - golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect - golang.org/x/text v0.3.7 // indirect + golang.org/x/crypto v0.1.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/term v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ee8ddca7b..cbee5c5ec 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,18 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= -github.com/99designs/keyring v1.2.1 h1:tYLp1ULvO7i3fI5vE21ReQuj99QFSs7lGm0xWyJo87o= -github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= -github.com/AlecAivazis/survey/v2 v2.3.5 h1:A8cYupsAZkjaUmhtTYv3sSqc7LO5mp1XDfqe5E/9wRQ= -github.com/AlecAivazis/survey/v2 v2.3.5/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI= +github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= +github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk= +github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw= +github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI= github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e h1:ZU22z/2YRFLyf/P4ZwUYSdNCWsMEI0VeyrFoI2rAhJQ= github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= -github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= +github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= +github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= github.com/alecthomas/kingpin v2.2.6+incompatible h1:5svnBTFgJjZvGKyYBtMB0+m5wvrbUHiqye8wRJMlnYI= github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= @@ -24,12 +24,12 @@ github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEq github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= -github.com/aws/aws-sdk-go v1.44.59 h1:bkdnNsMvMhFmNLqKDAJ6rKR+S0hjOt/3AIJp2mxOK9o= -github.com/aws/aws-sdk-go v1.44.59/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.281 h1:z/ptheJvINaIAsKXthxONM+toTKw2pxyk700Hfm6yUw= +github.com/aws/aws-sdk-go v1.44.281/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/bearsh/hid v1.3.0 h1:GLNa8hvEzJxzQEEpheDUr2SivvH7iwTrJrDhFKutfX8= github.com/bearsh/hid v1.3.0/go.mod h1:KbQByg8WfPr92v7aaKAHTtZUEVG7e2XRpcF8+TopQv8= -github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= -github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= +github.com/beevik/etree v1.2.0 h1:l7WETslUG/T+xOPs47dtd6jov2Ii/8/OjCldk5fYfQw= +github.com/beevik/etree v1.2.0/go.mod h1:aiPf89g/1k3AShMVAzriilpcE4R/Vuor90y83zVZWFc= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -42,8 +42,8 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= -github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= +github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= +github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -59,6 +59,8 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= +github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -73,7 +75,6 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= @@ -81,6 +82,10 @@ github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/h2non/filetype v1.1.1/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= +github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= +github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= @@ -120,8 +125,8 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mxschmitt/playwright-go v0.1400.0 h1:HL8dbxcVEobE+pNjASeYGJJRmd4+9gyu/51XO7d3qF0= -github.com/mxschmitt/playwright-go v0.1400.0/go.mod h1:kUvZFgMneRGknVLtC2DKQ42lhZiCmWzxgBdGwjC0vkw= +github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= +github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= @@ -129,6 +134,8 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/playwright-community/playwright-go v0.2000.1 h1:2JViSHpJQ/UL/PO1Gg6gXV5IcXAAsoBJ3KG9L3wKXto= +github.com/playwright-community/playwright-go v0.2000.1/go.mod h1:1y9cM9b9dVHnuRWzED1KLM7FtbwTJC8ibDjI6MNqewU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -145,8 +152,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -159,16 +166,18 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/tidwall/gjson v1.14.1 h1:iymTbGkQBhveq21bEvAQ81I0LEBork8BFe1CUZXdyuo= -github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= @@ -177,59 +186,79 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1 github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM= -golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -239,8 +268,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI= -gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= diff --git a/helper/osxkeychain/keychain.go b/helper/osxkeychain/keychain.go index 3a9ec5ba7..555655f02 100644 --- a/helper/osxkeychain/keychain.go +++ b/helper/osxkeychain/keychain.go @@ -1,3 +1,4 @@ +//go:build darwin && cgo // +build darwin,cgo package osxkeychain diff --git a/helper/osxkeychain/osxkeychain.go b/helper/osxkeychain/osxkeychain.go index cc0f951b0..93e2bcdf3 100644 --- a/helper/osxkeychain/osxkeychain.go +++ b/helper/osxkeychain/osxkeychain.go @@ -1,3 +1,4 @@ +//go:build darwin && cgo // +build darwin,cgo package osxkeychain diff --git a/helper/osxkeychain/osxkeychain_test.go b/helper/osxkeychain/osxkeychain_test.go index de9cda899..78b85337c 100644 --- a/helper/osxkeychain/osxkeychain_test.go +++ b/helper/osxkeychain/osxkeychain_test.go @@ -1,3 +1,4 @@ +//go:build darwin && cgo // +build darwin,cgo // Copyright (c) 2016 David Calavera diff --git a/mocks/Page.go b/mocks/Page.go new file mode 100644 index 000000000..ae182099a --- /dev/null +++ b/mocks/Page.go @@ -0,0 +1,2132 @@ +// Code generated by mockery v2.22.1. DO NOT EDIT. + +package mocks + +import ( + playwright "github.com/playwright-community/playwright-go" + mock "github.com/stretchr/testify/mock" +) + +// Page is an autogenerated mock type for the Page type +type Page struct { + mock.Mock +} + +// AddInitScript provides a mock function with given fields: script +func (_m *Page) AddInitScript(script playwright.PageAddInitScriptOptions) error { + ret := _m.Called(script) + + var r0 error + if rf, ok := ret.Get(0).(func(playwright.PageAddInitScriptOptions) error); ok { + r0 = rf(script) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// AddScriptTag provides a mock function with given fields: options +func (_m *Page) AddScriptTag(options playwright.PageAddScriptTagOptions) (playwright.ElementHandle, error) { + ret := _m.Called(options) + + var r0 playwright.ElementHandle + var r1 error + if rf, ok := ret.Get(0).(func(playwright.PageAddScriptTagOptions) (playwright.ElementHandle, error)); ok { + return rf(options) + } + if rf, ok := ret.Get(0).(func(playwright.PageAddScriptTagOptions) playwright.ElementHandle); ok { + r0 = rf(options) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.ElementHandle) + } + } + + if rf, ok := ret.Get(1).(func(playwright.PageAddScriptTagOptions) error); ok { + r1 = rf(options) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// AddStyleTag provides a mock function with given fields: options +func (_m *Page) AddStyleTag(options playwright.PageAddStyleTagOptions) (playwright.ElementHandle, error) { + ret := _m.Called(options) + + var r0 playwright.ElementHandle + var r1 error + if rf, ok := ret.Get(0).(func(playwright.PageAddStyleTagOptions) (playwright.ElementHandle, error)); ok { + return rf(options) + } + if rf, ok := ret.Get(0).(func(playwright.PageAddStyleTagOptions) playwright.ElementHandle); ok { + r0 = rf(options) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.ElementHandle) + } + } + + if rf, ok := ret.Get(1).(func(playwright.PageAddStyleTagOptions) error); ok { + r1 = rf(options) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BringToFront provides a mock function with given fields: +func (_m *Page) BringToFront() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Check provides a mock function with given fields: selector, options +func (_m *Page) Check(selector string, options ...playwright.FrameCheckOptions) error { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, selector) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(string, ...playwright.FrameCheckOptions) error); ok { + r0 = rf(selector, options...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Click provides a mock function with given fields: selector, options +func (_m *Page) Click(selector string, options ...playwright.PageClickOptions) error { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, selector) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(string, ...playwright.PageClickOptions) error); ok { + r0 = rf(selector, options...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Close provides a mock function with given fields: options +func (_m *Page) Close(options ...playwright.PageCloseOptions) error { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(...playwright.PageCloseOptions) error); ok { + r0 = rf(options...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Content provides a mock function with given fields: +func (_m *Page) Content() (string, error) { + ret := _m.Called() + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func() (string, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Context provides a mock function with given fields: +func (_m *Page) Context() playwright.BrowserContext { + ret := _m.Called() + + var r0 playwright.BrowserContext + if rf, ok := ret.Get(0).(func() playwright.BrowserContext); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.BrowserContext) + } + } + + return r0 +} + +// Dblclick provides a mock function with given fields: expression, options +func (_m *Page) Dblclick(expression string, options ...playwright.FrameDblclickOptions) error { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, expression) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(string, ...playwright.FrameDblclickOptions) error); ok { + r0 = rf(expression, options...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DispatchEvent provides a mock function with given fields: selector, typ, options +func (_m *Page) DispatchEvent(selector string, typ string, options ...playwright.PageDispatchEventOptions) error { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, selector, typ) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string, ...playwright.PageDispatchEventOptions) error); ok { + r0 = rf(selector, typ, options...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DragAndDrop provides a mock function with given fields: source, target, options +func (_m *Page) DragAndDrop(source string, target string, options ...playwright.FrameDragAndDropOptions) error { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, source, target) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string, ...playwright.FrameDragAndDropOptions) error); ok { + r0 = rf(source, target, options...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Emit provides a mock function with given fields: name, payload +func (_m *Page) Emit(name string, payload ...interface{}) { + var _ca []interface{} + _ca = append(_ca, name) + _ca = append(_ca, payload...) + _m.Called(_ca...) +} + +// EmulateMedia provides a mock function with given fields: options +func (_m *Page) EmulateMedia(options ...playwright.PageEmulateMediaOptions) error { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(...playwright.PageEmulateMediaOptions) error); ok { + r0 = rf(options...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// EvalOnSelector provides a mock function with given fields: selector, expression, options +func (_m *Page) EvalOnSelector(selector string, expression string, options ...interface{}) (interface{}, error) { + var _ca []interface{} + _ca = append(_ca, selector, expression) + _ca = append(_ca, options...) + ret := _m.Called(_ca...) + + var r0 interface{} + var r1 error + if rf, ok := ret.Get(0).(func(string, string, ...interface{}) (interface{}, error)); ok { + return rf(selector, expression, options...) + } + if rf, ok := ret.Get(0).(func(string, string, ...interface{}) interface{}); ok { + r0 = rf(selector, expression, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(interface{}) + } + } + + if rf, ok := ret.Get(1).(func(string, string, ...interface{}) error); ok { + r1 = rf(selector, expression, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// EvalOnSelectorAll provides a mock function with given fields: selector, expression, options +func (_m *Page) EvalOnSelectorAll(selector string, expression string, options ...interface{}) (interface{}, error) { + var _ca []interface{} + _ca = append(_ca, selector, expression) + _ca = append(_ca, options...) + ret := _m.Called(_ca...) + + var r0 interface{} + var r1 error + if rf, ok := ret.Get(0).(func(string, string, ...interface{}) (interface{}, error)); ok { + return rf(selector, expression, options...) + } + if rf, ok := ret.Get(0).(func(string, string, ...interface{}) interface{}); ok { + r0 = rf(selector, expression, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(interface{}) + } + } + + if rf, ok := ret.Get(1).(func(string, string, ...interface{}) error); ok { + r1 = rf(selector, expression, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Evaluate provides a mock function with given fields: expression, options +func (_m *Page) Evaluate(expression string, options ...interface{}) (interface{}, error) { + var _ca []interface{} + _ca = append(_ca, expression) + _ca = append(_ca, options...) + ret := _m.Called(_ca...) + + var r0 interface{} + var r1 error + if rf, ok := ret.Get(0).(func(string, ...interface{}) (interface{}, error)); ok { + return rf(expression, options...) + } + if rf, ok := ret.Get(0).(func(string, ...interface{}) interface{}); ok { + r0 = rf(expression, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(interface{}) + } + } + + if rf, ok := ret.Get(1).(func(string, ...interface{}) error); ok { + r1 = rf(expression, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// EvaluateHandle provides a mock function with given fields: expression, options +func (_m *Page) EvaluateHandle(expression string, options ...interface{}) (playwright.JSHandle, error) { + var _ca []interface{} + _ca = append(_ca, expression) + _ca = append(_ca, options...) + ret := _m.Called(_ca...) + + var r0 playwright.JSHandle + var r1 error + if rf, ok := ret.Get(0).(func(string, ...interface{}) (playwright.JSHandle, error)); ok { + return rf(expression, options...) + } + if rf, ok := ret.Get(0).(func(string, ...interface{}) playwright.JSHandle); ok { + r0 = rf(expression, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.JSHandle) + } + } + + if rf, ok := ret.Get(1).(func(string, ...interface{}) error); ok { + r1 = rf(expression, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ExpectConsoleMessage provides a mock function with given fields: cb +func (_m *Page) ExpectConsoleMessage(cb func() error) (playwright.ConsoleMessage, error) { + ret := _m.Called(cb) + + var r0 playwright.ConsoleMessage + var r1 error + if rf, ok := ret.Get(0).(func(func() error) (playwright.ConsoleMessage, error)); ok { + return rf(cb) + } + if rf, ok := ret.Get(0).(func(func() error) playwright.ConsoleMessage); ok { + r0 = rf(cb) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.ConsoleMessage) + } + } + + if rf, ok := ret.Get(1).(func(func() error) error); ok { + r1 = rf(cb) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ExpectDownload provides a mock function with given fields: cb +func (_m *Page) ExpectDownload(cb func() error) (playwright.Download, error) { + ret := _m.Called(cb) + + var r0 playwright.Download + var r1 error + if rf, ok := ret.Get(0).(func(func() error) (playwright.Download, error)); ok { + return rf(cb) + } + if rf, ok := ret.Get(0).(func(func() error) playwright.Download); ok { + r0 = rf(cb) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.Download) + } + } + + if rf, ok := ret.Get(1).(func(func() error) error); ok { + r1 = rf(cb) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ExpectEvent provides a mock function with given fields: event, cb, predicates +func (_m *Page) ExpectEvent(event string, cb func() error, predicates ...interface{}) (interface{}, error) { + var _ca []interface{} + _ca = append(_ca, event, cb) + _ca = append(_ca, predicates...) + ret := _m.Called(_ca...) + + var r0 interface{} + var r1 error + if rf, ok := ret.Get(0).(func(string, func() error, ...interface{}) (interface{}, error)); ok { + return rf(event, cb, predicates...) + } + if rf, ok := ret.Get(0).(func(string, func() error, ...interface{}) interface{}); ok { + r0 = rf(event, cb, predicates...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(interface{}) + } + } + + if rf, ok := ret.Get(1).(func(string, func() error, ...interface{}) error); ok { + r1 = rf(event, cb, predicates...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ExpectFileChooser provides a mock function with given fields: cb +func (_m *Page) ExpectFileChooser(cb func() error) (playwright.FileChooser, error) { + ret := _m.Called(cb) + + var r0 playwright.FileChooser + var r1 error + if rf, ok := ret.Get(0).(func(func() error) (playwright.FileChooser, error)); ok { + return rf(cb) + } + if rf, ok := ret.Get(0).(func(func() error) playwright.FileChooser); ok { + r0 = rf(cb) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.FileChooser) + } + } + + if rf, ok := ret.Get(1).(func(func() error) error); ok { + r1 = rf(cb) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ExpectLoadState provides a mock function with given fields: state, cb +func (_m *Page) ExpectLoadState(state string, cb func() error) error { + ret := _m.Called(state, cb) + + var r0 error + if rf, ok := ret.Get(0).(func(string, func() error) error); ok { + r0 = rf(state, cb) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ExpectNavigation provides a mock function with given fields: cb, options +func (_m *Page) ExpectNavigation(cb func() error, options ...playwright.PageWaitForNavigationOptions) (playwright.Response, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, cb) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 playwright.Response + var r1 error + if rf, ok := ret.Get(0).(func(func() error, ...playwright.PageWaitForNavigationOptions) (playwright.Response, error)); ok { + return rf(cb, options...) + } + if rf, ok := ret.Get(0).(func(func() error, ...playwright.PageWaitForNavigationOptions) playwright.Response); ok { + r0 = rf(cb, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.Response) + } + } + + if rf, ok := ret.Get(1).(func(func() error, ...playwright.PageWaitForNavigationOptions) error); ok { + r1 = rf(cb, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ExpectPopup provides a mock function with given fields: cb +func (_m *Page) ExpectPopup(cb func() error) (playwright.Page, error) { + ret := _m.Called(cb) + + var r0 playwright.Page + var r1 error + if rf, ok := ret.Get(0).(func(func() error) (playwright.Page, error)); ok { + return rf(cb) + } + if rf, ok := ret.Get(0).(func(func() error) playwright.Page); ok { + r0 = rf(cb) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.Page) + } + } + + if rf, ok := ret.Get(1).(func(func() error) error); ok { + r1 = rf(cb) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ExpectRequest provides a mock function with given fields: url, cb, options +func (_m *Page) ExpectRequest(url interface{}, cb func() error, options ...interface{}) (playwright.Request, error) { + var _ca []interface{} + _ca = append(_ca, url, cb) + _ca = append(_ca, options...) + ret := _m.Called(_ca...) + + var r0 playwright.Request + var r1 error + if rf, ok := ret.Get(0).(func(interface{}, func() error, ...interface{}) (playwright.Request, error)); ok { + return rf(url, cb, options...) + } + if rf, ok := ret.Get(0).(func(interface{}, func() error, ...interface{}) playwright.Request); ok { + r0 = rf(url, cb, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.Request) + } + } + + if rf, ok := ret.Get(1).(func(interface{}, func() error, ...interface{}) error); ok { + r1 = rf(url, cb, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ExpectResponse provides a mock function with given fields: url, cb, options +func (_m *Page) ExpectResponse(url interface{}, cb func() error, options ...interface{}) (playwright.Response, error) { + var _ca []interface{} + _ca = append(_ca, url, cb) + _ca = append(_ca, options...) + ret := _m.Called(_ca...) + + var r0 playwright.Response + var r1 error + if rf, ok := ret.Get(0).(func(interface{}, func() error, ...interface{}) (playwright.Response, error)); ok { + return rf(url, cb, options...) + } + if rf, ok := ret.Get(0).(func(interface{}, func() error, ...interface{}) playwright.Response); ok { + r0 = rf(url, cb, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.Response) + } + } + + if rf, ok := ret.Get(1).(func(interface{}, func() error, ...interface{}) error); ok { + r1 = rf(url, cb, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ExpectWorker provides a mock function with given fields: cb +func (_m *Page) ExpectWorker(cb func() error) (playwright.Worker, error) { + ret := _m.Called(cb) + + var r0 playwright.Worker + var r1 error + if rf, ok := ret.Get(0).(func(func() error) (playwright.Worker, error)); ok { + return rf(cb) + } + if rf, ok := ret.Get(0).(func(func() error) playwright.Worker); ok { + r0 = rf(cb) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.Worker) + } + } + + if rf, ok := ret.Get(1).(func(func() error) error); ok { + r1 = rf(cb) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ExpectedDialog provides a mock function with given fields: cb +func (_m *Page) ExpectedDialog(cb func() error) (playwright.Dialog, error) { + ret := _m.Called(cb) + + var r0 playwright.Dialog + var r1 error + if rf, ok := ret.Get(0).(func(func() error) (playwright.Dialog, error)); ok { + return rf(cb) + } + if rf, ok := ret.Get(0).(func(func() error) playwright.Dialog); ok { + r0 = rf(cb) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.Dialog) + } + } + + if rf, ok := ret.Get(1).(func(func() error) error); ok { + r1 = rf(cb) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ExposeBinding provides a mock function with given fields: name, binding, handle +func (_m *Page) ExposeBinding(name string, binding playwright.BindingCallFunction, handle ...bool) error { + _va := make([]interface{}, len(handle)) + for _i := range handle { + _va[_i] = handle[_i] + } + var _ca []interface{} + _ca = append(_ca, name, binding) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(string, playwright.BindingCallFunction, ...bool) error); ok { + r0 = rf(name, binding, handle...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ExposeFunction provides a mock function with given fields: name, binding +func (_m *Page) ExposeFunction(name string, binding func(...interface{}) interface{}) error { + ret := _m.Called(name, binding) + + var r0 error + if rf, ok := ret.Get(0).(func(string, func(...interface{}) interface{}) error); ok { + r0 = rf(name, binding) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Fill provides a mock function with given fields: selector, text, options +func (_m *Page) Fill(selector string, text string, options ...playwright.FrameFillOptions) error { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, selector, text) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string, ...playwright.FrameFillOptions) error); ok { + r0 = rf(selector, text, options...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Focus provides a mock function with given fields: expression, options +func (_m *Page) Focus(expression string, options ...playwright.FrameFocusOptions) error { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, expression) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(string, ...playwright.FrameFocusOptions) error); ok { + r0 = rf(expression, options...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Frame provides a mock function with given fields: options +func (_m *Page) Frame(options playwright.PageFrameOptions) playwright.Frame { + ret := _m.Called(options) + + var r0 playwright.Frame + if rf, ok := ret.Get(0).(func(playwright.PageFrameOptions) playwright.Frame); ok { + r0 = rf(options) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.Frame) + } + } + + return r0 +} + +// Frames provides a mock function with given fields: +func (_m *Page) Frames() []playwright.Frame { + ret := _m.Called() + + var r0 []playwright.Frame + if rf, ok := ret.Get(0).(func() []playwright.Frame); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]playwright.Frame) + } + } + + return r0 +} + +// GetAttribute provides a mock function with given fields: selector, name, options +func (_m *Page) GetAttribute(selector string, name string, options ...playwright.PageGetAttributeOptions) (string, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, selector, name) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(string, string, ...playwright.PageGetAttributeOptions) (string, error)); ok { + return rf(selector, name, options...) + } + if rf, ok := ret.Get(0).(func(string, string, ...playwright.PageGetAttributeOptions) string); ok { + r0 = rf(selector, name, options...) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(string, string, ...playwright.PageGetAttributeOptions) error); ok { + r1 = rf(selector, name, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GoBack provides a mock function with given fields: options +func (_m *Page) GoBack(options ...playwright.PageGoBackOptions) (playwright.Response, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 playwright.Response + var r1 error + if rf, ok := ret.Get(0).(func(...playwright.PageGoBackOptions) (playwright.Response, error)); ok { + return rf(options...) + } + if rf, ok := ret.Get(0).(func(...playwright.PageGoBackOptions) playwright.Response); ok { + r0 = rf(options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.Response) + } + } + + if rf, ok := ret.Get(1).(func(...playwright.PageGoBackOptions) error); ok { + r1 = rf(options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GoForward provides a mock function with given fields: options +func (_m *Page) GoForward(options ...playwright.PageGoForwardOptions) (playwright.Response, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 playwright.Response + var r1 error + if rf, ok := ret.Get(0).(func(...playwright.PageGoForwardOptions) (playwright.Response, error)); ok { + return rf(options...) + } + if rf, ok := ret.Get(0).(func(...playwright.PageGoForwardOptions) playwright.Response); ok { + r0 = rf(options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.Response) + } + } + + if rf, ok := ret.Get(1).(func(...playwright.PageGoForwardOptions) error); ok { + r1 = rf(options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Goto provides a mock function with given fields: url, options +func (_m *Page) Goto(url string, options ...playwright.PageGotoOptions) (playwright.Response, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, url) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 playwright.Response + var r1 error + if rf, ok := ret.Get(0).(func(string, ...playwright.PageGotoOptions) (playwright.Response, error)); ok { + return rf(url, options...) + } + if rf, ok := ret.Get(0).(func(string, ...playwright.PageGotoOptions) playwright.Response); ok { + r0 = rf(url, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.Response) + } + } + + if rf, ok := ret.Get(1).(func(string, ...playwright.PageGotoOptions) error); ok { + r1 = rf(url, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Hover provides a mock function with given fields: selector, options +func (_m *Page) Hover(selector string, options ...playwright.PageHoverOptions) error { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, selector) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(string, ...playwright.PageHoverOptions) error); ok { + r0 = rf(selector, options...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// InnerHTML provides a mock function with given fields: selector, options +func (_m *Page) InnerHTML(selector string, options ...playwright.PageInnerHTMLOptions) (string, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, selector) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(string, ...playwright.PageInnerHTMLOptions) (string, error)); ok { + return rf(selector, options...) + } + if rf, ok := ret.Get(0).(func(string, ...playwright.PageInnerHTMLOptions) string); ok { + r0 = rf(selector, options...) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(string, ...playwright.PageInnerHTMLOptions) error); ok { + r1 = rf(selector, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// InnerText provides a mock function with given fields: selector, options +func (_m *Page) InnerText(selector string, options ...playwright.PageInnerTextOptions) (string, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, selector) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(string, ...playwright.PageInnerTextOptions) (string, error)); ok { + return rf(selector, options...) + } + if rf, ok := ret.Get(0).(func(string, ...playwright.PageInnerTextOptions) string); ok { + r0 = rf(selector, options...) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(string, ...playwright.PageInnerTextOptions) error); ok { + r1 = rf(selector, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// InputValue provides a mock function with given fields: selector, options +func (_m *Page) InputValue(selector string, options ...playwright.FrameInputValueOptions) (string, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, selector) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(string, ...playwright.FrameInputValueOptions) (string, error)); ok { + return rf(selector, options...) + } + if rf, ok := ret.Get(0).(func(string, ...playwright.FrameInputValueOptions) string); ok { + r0 = rf(selector, options...) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(string, ...playwright.FrameInputValueOptions) error); ok { + r1 = rf(selector, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// IsChecked provides a mock function with given fields: selector, options +func (_m *Page) IsChecked(selector string, options ...playwright.FrameIsCheckedOptions) (bool, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, selector) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(string, ...playwright.FrameIsCheckedOptions) (bool, error)); ok { + return rf(selector, options...) + } + if rf, ok := ret.Get(0).(func(string, ...playwright.FrameIsCheckedOptions) bool); ok { + r0 = rf(selector, options...) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(string, ...playwright.FrameIsCheckedOptions) error); ok { + r1 = rf(selector, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// IsClosed provides a mock function with given fields: +func (_m *Page) IsClosed() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// IsDisabled provides a mock function with given fields: selector, options +func (_m *Page) IsDisabled(selector string, options ...playwright.FrameIsDisabledOptions) (bool, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, selector) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(string, ...playwright.FrameIsDisabledOptions) (bool, error)); ok { + return rf(selector, options...) + } + if rf, ok := ret.Get(0).(func(string, ...playwright.FrameIsDisabledOptions) bool); ok { + r0 = rf(selector, options...) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(string, ...playwright.FrameIsDisabledOptions) error); ok { + r1 = rf(selector, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// IsEditable provides a mock function with given fields: selector, options +func (_m *Page) IsEditable(selector string, options ...playwright.FrameIsEditableOptions) (bool, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, selector) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(string, ...playwright.FrameIsEditableOptions) (bool, error)); ok { + return rf(selector, options...) + } + if rf, ok := ret.Get(0).(func(string, ...playwright.FrameIsEditableOptions) bool); ok { + r0 = rf(selector, options...) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(string, ...playwright.FrameIsEditableOptions) error); ok { + r1 = rf(selector, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// IsEnabled provides a mock function with given fields: selector, options +func (_m *Page) IsEnabled(selector string, options ...playwright.FrameIsEnabledOptions) (bool, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, selector) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(string, ...playwright.FrameIsEnabledOptions) (bool, error)); ok { + return rf(selector, options...) + } + if rf, ok := ret.Get(0).(func(string, ...playwright.FrameIsEnabledOptions) bool); ok { + r0 = rf(selector, options...) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(string, ...playwright.FrameIsEnabledOptions) error); ok { + r1 = rf(selector, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// IsHidden provides a mock function with given fields: selector, options +func (_m *Page) IsHidden(selector string, options ...playwright.FrameIsHiddenOptions) (bool, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, selector) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(string, ...playwright.FrameIsHiddenOptions) (bool, error)); ok { + return rf(selector, options...) + } + if rf, ok := ret.Get(0).(func(string, ...playwright.FrameIsHiddenOptions) bool); ok { + r0 = rf(selector, options...) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(string, ...playwright.FrameIsHiddenOptions) error); ok { + r1 = rf(selector, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// IsVisible provides a mock function with given fields: selector, options +func (_m *Page) IsVisible(selector string, options ...playwright.FrameIsVisibleOptions) (bool, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, selector) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(string, ...playwright.FrameIsVisibleOptions) (bool, error)); ok { + return rf(selector, options...) + } + if rf, ok := ret.Get(0).(func(string, ...playwright.FrameIsVisibleOptions) bool); ok { + r0 = rf(selector, options...) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(string, ...playwright.FrameIsVisibleOptions) error); ok { + r1 = rf(selector, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Keyboard provides a mock function with given fields: +func (_m *Page) Keyboard() playwright.Keyboard { + ret := _m.Called() + + var r0 playwright.Keyboard + if rf, ok := ret.Get(0).(func() playwright.Keyboard); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.Keyboard) + } + } + + return r0 +} + +// ListenerCount provides a mock function with given fields: name +func (_m *Page) ListenerCount(name string) int { + ret := _m.Called(name) + + var r0 int + if rf, ok := ret.Get(0).(func(string) int); ok { + r0 = rf(name) + } else { + r0 = ret.Get(0).(int) + } + + return r0 +} + +// Locator provides a mock function with given fields: selector, options +func (_m *Page) Locator(selector string, options ...playwright.PageLocatorOptions) (playwright.Locator, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, selector) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 playwright.Locator + var r1 error + if rf, ok := ret.Get(0).(func(string, ...playwright.PageLocatorOptions) (playwright.Locator, error)); ok { + return rf(selector, options...) + } + if rf, ok := ret.Get(0).(func(string, ...playwright.PageLocatorOptions) playwright.Locator); ok { + r0 = rf(selector, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.Locator) + } + } + + if rf, ok := ret.Get(1).(func(string, ...playwright.PageLocatorOptions) error); ok { + r1 = rf(selector, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MainFrame provides a mock function with given fields: +func (_m *Page) MainFrame() playwright.Frame { + ret := _m.Called() + + var r0 playwright.Frame + if rf, ok := ret.Get(0).(func() playwright.Frame); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.Frame) + } + } + + return r0 +} + +// Mouse provides a mock function with given fields: +func (_m *Page) Mouse() playwright.Mouse { + ret := _m.Called() + + var r0 playwright.Mouse + if rf, ok := ret.Get(0).(func() playwright.Mouse); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.Mouse) + } + } + + return r0 +} + +// On provides a mock function with given fields: name, handler +func (_m *Page) On(name string, handler interface{}) { + _m.Called(name, handler) +} + +// Once provides a mock function with given fields: name, handler +func (_m *Page) Once(name string, handler interface{}) { + _m.Called(name, handler) +} + +// Opener provides a mock function with given fields: +func (_m *Page) Opener() (playwright.Page, error) { + ret := _m.Called() + + var r0 playwright.Page + var r1 error + if rf, ok := ret.Get(0).(func() (playwright.Page, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() playwright.Page); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.Page) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// PDF provides a mock function with given fields: options +func (_m *Page) PDF(options ...playwright.PagePdfOptions) ([]byte, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func(...playwright.PagePdfOptions) ([]byte, error)); ok { + return rf(options...) + } + if rf, ok := ret.Get(0).(func(...playwright.PagePdfOptions) []byte); ok { + r0 = rf(options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func(...playwright.PagePdfOptions) error); ok { + r1 = rf(options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Pause provides a mock function with given fields: +func (_m *Page) Pause() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Press provides a mock function with given fields: selector, key, options +func (_m *Page) Press(selector string, key string, options ...playwright.PagePressOptions) error { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, selector, key) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string, ...playwright.PagePressOptions) error); ok { + r0 = rf(selector, key, options...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// QuerySelector provides a mock function with given fields: selector +func (_m *Page) QuerySelector(selector string) (playwright.ElementHandle, error) { + ret := _m.Called(selector) + + var r0 playwright.ElementHandle + var r1 error + if rf, ok := ret.Get(0).(func(string) (playwright.ElementHandle, error)); ok { + return rf(selector) + } + if rf, ok := ret.Get(0).(func(string) playwright.ElementHandle); ok { + r0 = rf(selector) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.ElementHandle) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(selector) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// QuerySelectorAll provides a mock function with given fields: selector +func (_m *Page) QuerySelectorAll(selector string) ([]playwright.ElementHandle, error) { + ret := _m.Called(selector) + + var r0 []playwright.ElementHandle + var r1 error + if rf, ok := ret.Get(0).(func(string) ([]playwright.ElementHandle, error)); ok { + return rf(selector) + } + if rf, ok := ret.Get(0).(func(string) []playwright.ElementHandle); ok { + r0 = rf(selector) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]playwright.ElementHandle) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(selector) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Reload provides a mock function with given fields: options +func (_m *Page) Reload(options ...playwright.PageReloadOptions) (playwright.Response, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 playwright.Response + var r1 error + if rf, ok := ret.Get(0).(func(...playwright.PageReloadOptions) (playwright.Response, error)); ok { + return rf(options...) + } + if rf, ok := ret.Get(0).(func(...playwright.PageReloadOptions) playwright.Response); ok { + r0 = rf(options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.Response) + } + } + + if rf, ok := ret.Get(1).(func(...playwright.PageReloadOptions) error); ok { + r1 = rf(options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RemoveListener provides a mock function with given fields: name, handler +func (_m *Page) RemoveListener(name string, handler interface{}) { + _m.Called(name, handler) +} + +// Route provides a mock function with given fields: url, handler +func (_m *Page) Route(url interface{}, handler func(playwright.Route, playwright.Request)) error { + ret := _m.Called(url, handler) + + var r0 error + if rf, ok := ret.Get(0).(func(interface{}, func(playwright.Route, playwright.Request)) error); ok { + r0 = rf(url, handler) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Screenshot provides a mock function with given fields: options +func (_m *Page) Screenshot(options ...playwright.PageScreenshotOptions) ([]byte, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func(...playwright.PageScreenshotOptions) ([]byte, error)); ok { + return rf(options...) + } + if rf, ok := ret.Get(0).(func(...playwright.PageScreenshotOptions) []byte); ok { + r0 = rf(options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func(...playwright.PageScreenshotOptions) error); ok { + r1 = rf(options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SelectOption provides a mock function with given fields: selector, values, options +func (_m *Page) SelectOption(selector string, values playwright.SelectOptionValues, options ...playwright.FrameSelectOptionOptions) ([]string, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, selector, values) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(string, playwright.SelectOptionValues, ...playwright.FrameSelectOptionOptions) ([]string, error)); ok { + return rf(selector, values, options...) + } + if rf, ok := ret.Get(0).(func(string, playwright.SelectOptionValues, ...playwright.FrameSelectOptionOptions) []string); ok { + r0 = rf(selector, values, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(string, playwright.SelectOptionValues, ...playwright.FrameSelectOptionOptions) error); ok { + r1 = rf(selector, values, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SetChecked provides a mock function with given fields: selector, checked, options +func (_m *Page) SetChecked(selector string, checked bool, options ...playwright.FrameSetCheckedOptions) error { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, selector, checked) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(string, bool, ...playwright.FrameSetCheckedOptions) error); ok { + r0 = rf(selector, checked, options...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SetContent provides a mock function with given fields: content, options +func (_m *Page) SetContent(content string, options ...playwright.PageSetContentOptions) error { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, content) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(string, ...playwright.PageSetContentOptions) error); ok { + r0 = rf(content, options...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SetDefaultNavigationTimeout provides a mock function with given fields: timeout +func (_m *Page) SetDefaultNavigationTimeout(timeout float64) { + _m.Called(timeout) +} + +// SetDefaultTimeout provides a mock function with given fields: timeout +func (_m *Page) SetDefaultTimeout(timeout float64) { + _m.Called(timeout) +} + +// SetExtraHTTPHeaders provides a mock function with given fields: headers +func (_m *Page) SetExtraHTTPHeaders(headers map[string]string) error { + ret := _m.Called(headers) + + var r0 error + if rf, ok := ret.Get(0).(func(map[string]string) error); ok { + r0 = rf(headers) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SetInputFiles provides a mock function with given fields: selector, files, options +func (_m *Page) SetInputFiles(selector string, files []playwright.InputFile, options ...playwright.FrameSetInputFilesOptions) error { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, selector, files) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(string, []playwright.InputFile, ...playwright.FrameSetInputFilesOptions) error); ok { + r0 = rf(selector, files, options...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SetViewportSize provides a mock function with given fields: width, height +func (_m *Page) SetViewportSize(width int, height int) error { + ret := _m.Called(width, height) + + var r0 error + if rf, ok := ret.Get(0).(func(int, int) error); ok { + r0 = rf(width, height) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Tap provides a mock function with given fields: selector, options +func (_m *Page) Tap(selector string, options ...playwright.FrameTapOptions) error { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, selector) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(string, ...playwright.FrameTapOptions) error); ok { + r0 = rf(selector, options...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// TextContent provides a mock function with given fields: selector, options +func (_m *Page) TextContent(selector string, options ...playwright.FrameTextContentOptions) (string, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, selector) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(string, ...playwright.FrameTextContentOptions) (string, error)); ok { + return rf(selector, options...) + } + if rf, ok := ret.Get(0).(func(string, ...playwright.FrameTextContentOptions) string); ok { + r0 = rf(selector, options...) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(string, ...playwright.FrameTextContentOptions) error); ok { + r1 = rf(selector, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Title provides a mock function with given fields: +func (_m *Page) Title() (string, error) { + ret := _m.Called() + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func() (string, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Touchscreen provides a mock function with given fields: +func (_m *Page) Touchscreen() playwright.Touchscreen { + ret := _m.Called() + + var r0 playwright.Touchscreen + if rf, ok := ret.Get(0).(func() playwright.Touchscreen); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.Touchscreen) + } + } + + return r0 +} + +// Type provides a mock function with given fields: selector, text, options +func (_m *Page) Type(selector string, text string, options ...playwright.PageTypeOptions) error { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, selector, text) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string, ...playwright.PageTypeOptions) error); ok { + r0 = rf(selector, text, options...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// URL provides a mock function with given fields: +func (_m *Page) URL() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Uncheck provides a mock function with given fields: selector, options +func (_m *Page) Uncheck(selector string, options ...playwright.FrameUncheckOptions) error { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, selector) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(string, ...playwright.FrameUncheckOptions) error); ok { + r0 = rf(selector, options...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Unroute provides a mock function with given fields: url, handler +func (_m *Page) Unroute(url interface{}, handler ...func(playwright.Route, playwright.Request)) error { + _va := make([]interface{}, len(handler)) + for _i := range handler { + _va[_i] = handler[_i] + } + var _ca []interface{} + _ca = append(_ca, url) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(interface{}, ...func(playwright.Route, playwright.Request)) error); ok { + r0 = rf(url, handler...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Video provides a mock function with given fields: +func (_m *Page) Video() playwright.Video { + ret := _m.Called() + + var r0 playwright.Video + if rf, ok := ret.Get(0).(func() playwright.Video); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.Video) + } + } + + return r0 +} + +// ViewportSize provides a mock function with given fields: +func (_m *Page) ViewportSize() playwright.ViewportSize { + ret := _m.Called() + + var r0 playwright.ViewportSize + if rf, ok := ret.Get(0).(func() playwright.ViewportSize); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(playwright.ViewportSize) + } + + return r0 +} + +// WaitForEvent provides a mock function with given fields: event, predicate +func (_m *Page) WaitForEvent(event string, predicate ...interface{}) interface{} { + var _ca []interface{} + _ca = append(_ca, event) + _ca = append(_ca, predicate...) + ret := _m.Called(_ca...) + + var r0 interface{} + if rf, ok := ret.Get(0).(func(string, ...interface{}) interface{}); ok { + r0 = rf(event, predicate...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(interface{}) + } + } + + return r0 +} + +// WaitForFunction provides a mock function with given fields: expression, arg, options +func (_m *Page) WaitForFunction(expression string, arg interface{}, options ...playwright.FrameWaitForFunctionOptions) (playwright.JSHandle, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, expression, arg) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 playwright.JSHandle + var r1 error + if rf, ok := ret.Get(0).(func(string, interface{}, ...playwright.FrameWaitForFunctionOptions) (playwright.JSHandle, error)); ok { + return rf(expression, arg, options...) + } + if rf, ok := ret.Get(0).(func(string, interface{}, ...playwright.FrameWaitForFunctionOptions) playwright.JSHandle); ok { + r0 = rf(expression, arg, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.JSHandle) + } + } + + if rf, ok := ret.Get(1).(func(string, interface{}, ...playwright.FrameWaitForFunctionOptions) error); ok { + r1 = rf(expression, arg, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// WaitForLoadState provides a mock function with given fields: state +func (_m *Page) WaitForLoadState(state ...string) { + _va := make([]interface{}, len(state)) + for _i := range state { + _va[_i] = state[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + _m.Called(_ca...) +} + +// WaitForNavigation provides a mock function with given fields: options +func (_m *Page) WaitForNavigation(options ...playwright.PageWaitForNavigationOptions) (playwright.Response, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 playwright.Response + var r1 error + if rf, ok := ret.Get(0).(func(...playwright.PageWaitForNavigationOptions) (playwright.Response, error)); ok { + return rf(options...) + } + if rf, ok := ret.Get(0).(func(...playwright.PageWaitForNavigationOptions) playwright.Response); ok { + r0 = rf(options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.Response) + } + } + + if rf, ok := ret.Get(1).(func(...playwright.PageWaitForNavigationOptions) error); ok { + r1 = rf(options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// WaitForRequest provides a mock function with given fields: url, options +func (_m *Page) WaitForRequest(url interface{}, options ...interface{}) playwright.Request { + var _ca []interface{} + _ca = append(_ca, url) + _ca = append(_ca, options...) + ret := _m.Called(_ca...) + + var r0 playwright.Request + if rf, ok := ret.Get(0).(func(interface{}, ...interface{}) playwright.Request); ok { + r0 = rf(url, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.Request) + } + } + + return r0 +} + +// WaitForResponse provides a mock function with given fields: url, options +func (_m *Page) WaitForResponse(url interface{}, options ...interface{}) playwright.Response { + var _ca []interface{} + _ca = append(_ca, url) + _ca = append(_ca, options...) + ret := _m.Called(_ca...) + + var r0 playwright.Response + if rf, ok := ret.Get(0).(func(interface{}, ...interface{}) playwright.Response); ok { + r0 = rf(url, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.Response) + } + } + + return r0 +} + +// WaitForSelector provides a mock function with given fields: selector, options +func (_m *Page) WaitForSelector(selector string, options ...playwright.PageWaitForSelectorOptions) (playwright.ElementHandle, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, selector) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 playwright.ElementHandle + var r1 error + if rf, ok := ret.Get(0).(func(string, ...playwright.PageWaitForSelectorOptions) (playwright.ElementHandle, error)); ok { + return rf(selector, options...) + } + if rf, ok := ret.Get(0).(func(string, ...playwright.PageWaitForSelectorOptions) playwright.ElementHandle); ok { + r0 = rf(selector, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.ElementHandle) + } + } + + if rf, ok := ret.Get(1).(func(string, ...playwright.PageWaitForSelectorOptions) error); ok { + r1 = rf(selector, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// WaitForTimeout provides a mock function with given fields: timeout +func (_m *Page) WaitForTimeout(timeout float64) { + _m.Called(timeout) +} + +// WaitForURL provides a mock function with given fields: url, options +func (_m *Page) WaitForURL(url string, options ...playwright.FrameWaitForURLOptions) error { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, url) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(string, ...playwright.FrameWaitForURLOptions) error); ok { + r0 = rf(url, options...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Workers provides a mock function with given fields: +func (_m *Page) Workers() []playwright.Worker { + ret := _m.Called() + + var r0 []playwright.Worker + if rf, ok := ret.Get(0).(func() []playwright.Worker); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]playwright.Worker) + } + } + + return r0 +} + +type mockConstructorTestingTNewPage interface { + mock.TestingT + Cleanup(func()) +} + +// NewPage creates a new instance of Page. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewPage(t mockConstructorTestingTNewPage) *Page { + mock := &Page{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/Request.go b/mocks/Request.go new file mode 100644 index 000000000..9c0233e4c --- /dev/null +++ b/mocks/Request.go @@ -0,0 +1,398 @@ +// Code generated by mockery v2.22.1. DO NOT EDIT. + +package mocks + +import ( + playwright "github.com/playwright-community/playwright-go" + mock "github.com/stretchr/testify/mock" +) + +// Request is an autogenerated mock type for the Request type +type Request struct { + mock.Mock +} + +// AllHeaders provides a mock function with given fields: +func (_m *Request) AllHeaders() (map[string]string, error) { + ret := _m.Called() + + var r0 map[string]string + var r1 error + if rf, ok := ret.Get(0).(func() (map[string]string, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() map[string]string); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]string) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Failure provides a mock function with given fields: +func (_m *Request) Failure() *playwright.RequestFailure { + ret := _m.Called() + + var r0 *playwright.RequestFailure + if rf, ok := ret.Get(0).(func() *playwright.RequestFailure); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*playwright.RequestFailure) + } + } + + return r0 +} + +// Frame provides a mock function with given fields: +func (_m *Request) Frame() playwright.Frame { + ret := _m.Called() + + var r0 playwright.Frame + if rf, ok := ret.Get(0).(func() playwright.Frame); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.Frame) + } + } + + return r0 +} + +// HeaderValue provides a mock function with given fields: name +func (_m *Request) HeaderValue(name string) (string, error) { + ret := _m.Called(name) + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(string) (string, error)); ok { + return rf(name) + } + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(name) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// HeaderValues provides a mock function with given fields: name +func (_m *Request) HeaderValues(name string) ([]string, error) { + ret := _m.Called(name) + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(string) ([]string, error)); ok { + return rf(name) + } + if rf, ok := ret.Get(0).(func(string) []string); ok { + r0 = rf(name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Headers provides a mock function with given fields: +func (_m *Request) Headers() map[string]string { + ret := _m.Called() + + var r0 map[string]string + if rf, ok := ret.Get(0).(func() map[string]string); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]string) + } + } + + return r0 +} + +// HeadersArray provides a mock function with given fields: +func (_m *Request) HeadersArray() (playwright.HeadersArray, error) { + ret := _m.Called() + + var r0 playwright.HeadersArray + var r1 error + if rf, ok := ret.Get(0).(func() (playwright.HeadersArray, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() playwright.HeadersArray); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.HeadersArray) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// IsNavigationRequest provides a mock function with given fields: +func (_m *Request) IsNavigationRequest() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// Method provides a mock function with given fields: +func (_m *Request) Method() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// PostData provides a mock function with given fields: +func (_m *Request) PostData() (string, error) { + ret := _m.Called() + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func() (string, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// PostDataBuffer provides a mock function with given fields: +func (_m *Request) PostDataBuffer() ([]byte, error) { + ret := _m.Called() + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func() ([]byte, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() []byte); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// PostDataJSON provides a mock function with given fields: v +func (_m *Request) PostDataJSON(v interface{}) error { + ret := _m.Called(v) + + var r0 error + if rf, ok := ret.Get(0).(func(interface{}) error); ok { + r0 = rf(v) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RedirectedFrom provides a mock function with given fields: +func (_m *Request) RedirectedFrom() playwright.Request { + ret := _m.Called() + + var r0 playwright.Request + if rf, ok := ret.Get(0).(func() playwright.Request); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.Request) + } + } + + return r0 +} + +// RedirectedTo provides a mock function with given fields: +func (_m *Request) RedirectedTo() playwright.Request { + ret := _m.Called() + + var r0 playwright.Request + if rf, ok := ret.Get(0).(func() playwright.Request); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.Request) + } + } + + return r0 +} + +// ResourceType provides a mock function with given fields: +func (_m *Request) ResourceType() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Response provides a mock function with given fields: +func (_m *Request) Response() (playwright.Response, error) { + ret := _m.Called() + + var r0 playwright.Response + var r1 error + if rf, ok := ret.Get(0).(func() (playwright.Response, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() playwright.Response); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.Response) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Sizes provides a mock function with given fields: +func (_m *Request) Sizes() (*playwright.RequestSizesResult, error) { + ret := _m.Called() + + var r0 *playwright.RequestSizesResult + var r1 error + if rf, ok := ret.Get(0).(func() (*playwright.RequestSizesResult, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() *playwright.RequestSizesResult); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*playwright.RequestSizesResult) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Timing provides a mock function with given fields: +func (_m *Request) Timing() *playwright.ResourceTiming { + ret := _m.Called() + + var r0 *playwright.ResourceTiming + if rf, ok := ret.Get(0).(func() *playwright.ResourceTiming); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*playwright.ResourceTiming) + } + } + + return r0 +} + +// URL provides a mock function with given fields: +func (_m *Request) URL() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +type mockConstructorTestingTNewRequest interface { + mock.TestingT + Cleanup(func()) +} + +// NewRequest creates a new instance of Request. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewRequest(t mockConstructorTestingTNewRequest) *Request { + mock := &Request{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/Response.go b/mocks/Response.go new file mode 100644 index 000000000..48fa44d44 --- /dev/null +++ b/mocks/Response.go @@ -0,0 +1,355 @@ +// Code generated by mockery v2.22.1. DO NOT EDIT. + +package mocks + +import ( + playwright "github.com/playwright-community/playwright-go" + mock "github.com/stretchr/testify/mock" +) + +// Response is an autogenerated mock type for the Response type +type Response struct { + mock.Mock +} + +// AllHeaders provides a mock function with given fields: +func (_m *Response) AllHeaders() (map[string]string, error) { + ret := _m.Called() + + var r0 map[string]string + var r1 error + if rf, ok := ret.Get(0).(func() (map[string]string, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() map[string]string); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]string) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Body provides a mock function with given fields: +func (_m *Response) Body() ([]byte, error) { + ret := _m.Called() + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func() ([]byte, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() []byte); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Finished provides a mock function with given fields: +func (_m *Response) Finished() { + _m.Called() +} + +// Frame provides a mock function with given fields: +func (_m *Response) Frame() playwright.Frame { + ret := _m.Called() + + var r0 playwright.Frame + if rf, ok := ret.Get(0).(func() playwright.Frame); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.Frame) + } + } + + return r0 +} + +// HeaderValue provides a mock function with given fields: name +func (_m *Response) HeaderValue(name string) (string, error) { + ret := _m.Called(name) + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(string) (string, error)); ok { + return rf(name) + } + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(name) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// HeaderValues provides a mock function with given fields: name +func (_m *Response) HeaderValues(name string) ([]string, error) { + ret := _m.Called(name) + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(string) ([]string, error)); ok { + return rf(name) + } + if rf, ok := ret.Get(0).(func(string) []string); ok { + r0 = rf(name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Headers provides a mock function with given fields: +func (_m *Response) Headers() map[string]string { + ret := _m.Called() + + var r0 map[string]string + if rf, ok := ret.Get(0).(func() map[string]string); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]string) + } + } + + return r0 +} + +// HeadersArray provides a mock function with given fields: +func (_m *Response) HeadersArray() (playwright.HeadersArray, error) { + ret := _m.Called() + + var r0 playwright.HeadersArray + var r1 error + if rf, ok := ret.Get(0).(func() (playwright.HeadersArray, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() playwright.HeadersArray); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.HeadersArray) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// JSON provides a mock function with given fields: v +func (_m *Response) JSON(v interface{}) error { + ret := _m.Called(v) + + var r0 error + if rf, ok := ret.Get(0).(func(interface{}) error); ok { + r0 = rf(v) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Ok provides a mock function with given fields: +func (_m *Response) Ok() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// Request provides a mock function with given fields: +func (_m *Response) Request() playwright.Request { + ret := _m.Called() + + var r0 playwright.Request + if rf, ok := ret.Get(0).(func() playwright.Request); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(playwright.Request) + } + } + + return r0 +} + +// SecurityDetails provides a mock function with given fields: +func (_m *Response) SecurityDetails() (*playwright.ResponseSecurityDetailsResult, error) { + ret := _m.Called() + + var r0 *playwright.ResponseSecurityDetailsResult + var r1 error + if rf, ok := ret.Get(0).(func() (*playwright.ResponseSecurityDetailsResult, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() *playwright.ResponseSecurityDetailsResult); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*playwright.ResponseSecurityDetailsResult) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ServerAddr provides a mock function with given fields: +func (_m *Response) ServerAddr() (*playwright.ResponseServerAddrResult, error) { + ret := _m.Called() + + var r0 *playwright.ResponseServerAddrResult + var r1 error + if rf, ok := ret.Get(0).(func() (*playwright.ResponseServerAddrResult, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() *playwright.ResponseServerAddrResult); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*playwright.ResponseServerAddrResult) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Status provides a mock function with given fields: +func (_m *Response) Status() int { + ret := _m.Called() + + var r0 int + if rf, ok := ret.Get(0).(func() int); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int) + } + + return r0 +} + +// StatusText provides a mock function with given fields: +func (_m *Response) StatusText() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Text provides a mock function with given fields: +func (_m *Response) Text() (string, error) { + ret := _m.Called() + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func() (string, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// URL provides a mock function with given fields: +func (_m *Response) URL() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +type mockConstructorTestingTNewResponse interface { + mock.TestingT + Cleanup(func()) +} + +// NewResponse creates a new instance of Response. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewResponse(t mockConstructorTestingTNewResponse) *Response { + mock := &Response{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/awsconfig/awsconfig.go b/pkg/awsconfig/awsconfig.go index 4d7317b19..b1bfef9dc 100644 --- a/pkg/awsconfig/awsconfig.go +++ b/pkg/awsconfig/awsconfig.go @@ -1,7 +1,6 @@ package awsconfig import ( - "io/ioutil" "os" "path" "path/filepath" @@ -143,7 +142,7 @@ func (p *CredentialsProvider) ensureConfigExists() error { logger.WithField("dir", dir).Debug("Dir created") // create an base config file - err = ioutil.WriteFile(filename, []byte("["+p.Profile+"]"), 0600) + err = os.WriteFile(filename, []byte("["+p.Profile+"]"), 0600) if err != nil { return err } diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go index ccfb19cc4..d547edf78 100644 --- a/pkg/cfg/cfg.go +++ b/pkg/cfg/cfg.go @@ -40,6 +40,7 @@ type IDPAccount struct { Username string `ini:"username"` Provider string `ini:"provider"` MFA string `ini:"mfa"` + MFAIPAddress string `ini:"mfa_ip_address"` // used by OneLogin SkipVerify bool `ini:"skip_verify"` Timeout int `ini:"timeout"` AmazonWebservicesURN string `ini:"aws_urn"` @@ -55,8 +56,11 @@ type IDPAccount struct { SAMLCache bool `ini:"saml_cache"` SAMLCacheFile string `ini:"saml_cache_file"` TargetURL string `ini:"target_url"` - DisableRememberDevice bool `ini:"disable_remember_device"` // used by Okta - DisableSessions bool `ini:"disable_sessions"` // used by Okta + DisableRememberDevice bool `ini:"disable_remember_device"` // used by Okta + DisableSessions bool `ini:"disable_sessions"` // used by Okta + DownloadBrowser bool `ini:"download_browser_driver"` // used by browser + BrowserDriverDir string `ini:"browser_driver_dir,omitempty"` // used by browser; hide from user if not set + Headless bool `ini:"headless"` // used by browser Prompter string `ini:"prompter"` } diff --git a/pkg/cfg/cfg_test.go b/pkg/cfg/cfg_test.go index b42802474..5a32a5c37 100644 --- a/pkg/cfg/cfg_test.go +++ b/pkg/cfg/cfg_test.go @@ -13,8 +13,28 @@ func TestNewConfigManagerNew(t *testing.T) { cfgm, err := NewConfigManager("example/saml2aws.ini") require.Nil(t, err) + require.NotNil(t, cfgm) +} + +func TestIDPAccountString(t *testing.T) { + cfgm, err := NewConfigManager("example/saml2aws.ini") + require.Nil(t, err) require.NotNil(t, cfgm) + + idpAccount, err := cfgm.LoadIDPAccount("test123") + require.Nil(t, err) + s := idpAccount.String() + require.Contains(t, s, "urn:amazon:webservices\n") +} + +func TestNewConfigManagerDefaultEmpty(t *testing.T) { + cfgm, err := NewConfigManager("") + require.Nil(t, err) + require.Contains(t, cfgm.configPath, ".saml2aws") + idpAccount, err := cfgm.LoadIDPAccount("foo") + require.Nil(t, err) + require.Equal(t, idpAccount.URL, "") } func TestNewConfigManagerLoad(t *testing.T) { diff --git a/pkg/cookiejar/jar.go b/pkg/cookiejar/jar.go index 62e5b26ec..2c3b569f8 100644 --- a/pkg/cookiejar/jar.go +++ b/pkg/cookiejar/jar.go @@ -18,9 +18,9 @@ import ( ) // PublicSuffixList provides the public suffix of a domain. For example: -// - the public suffix of "example.com" is "com", -// - the public suffix of "foo1.foo2.foo3.co.uk" is "co.uk", and -// - the public suffix of "bar.pvt.k12.ma.us" is "pvt.k12.ma.us". +// - the public suffix of "example.com" is "com", +// - the public suffix of "foo1.foo2.foo3.co.uk" is "co.uk", and +// - the public suffix of "bar.pvt.k12.ma.us" is "pvt.k12.ma.us". // // Implementations of PublicSuffixList must be safe for concurrent use by // multiple goroutines. diff --git a/pkg/cookiejar/jar_test.go b/pkg/cookiejar/jar_test.go index fc1462d0d..5262d2154 100644 --- a/pkg/cookiejar/jar_test.go +++ b/pkg/cookiejar/jar_test.go @@ -20,8 +20,9 @@ var tNow = time.Date(2013, 1, 1, 12, 0, 0, 0, time.UTC) // testPSL implements PublicSuffixList with just two rules: "co.uk" // and the default rule "*". // The implementation has two intentional bugs: -// PublicSuffix("www.buggy.psl") == "xy" -// PublicSuffix("www2.buggy.psl") == "com" +// +// PublicSuffix("www.buggy.psl") == "xy" +// PublicSuffix("www2.buggy.psl") == "com" type testPSL struct{} func (testPSL) String() string { @@ -358,13 +359,13 @@ func mustParseURL(s string) *url.URL { } // jarTest encapsulates the following actions on a jar: -// 1. Perform SetCookies with fromURL and the cookies from setCookies. -// (Done at time tNow + 0 ms.) -// 2. Check that the entries in the jar matches content. -// (Done at time tNow + 1001 ms.) -// 3. For each query in tests: Check that Cookies with toURL yields the -// cookies in want. -// (Query n done at tNow + (n+2)*1001 ms.) +// 1. Perform SetCookies with fromURL and the cookies from setCookies. +// (Done at time tNow + 0 ms.) +// 2. Check that the entries in the jar matches content. +// (Done at time tNow + 1001 ms.) +// 3. For each query in tests: Check that Cookies with toURL yields the +// cookies in want. +// (Query n done at tNow + (n+2)*1001 ms.) type jarTest struct { description string // The description of what this test is supposed to test fromURL string // The full URL of the request from which Set-Cookie headers where received diff --git a/pkg/creds/creds.go b/pkg/creds/creds.go index e006216ba..5aa697103 100644 --- a/pkg/creds/creds.go +++ b/pkg/creds/creds.go @@ -4,6 +4,8 @@ package creds type LoginDetails struct { ClientID string // used by OneLogin ClientSecret string // used by OneLogin + DownloadBrowser bool // used by Browser + MFAIPAddress string // used by OneLogin Username string Password string MFAToken string diff --git a/pkg/flags/flags.go b/pkg/flags/flags.go index 7a3beb6fa..60d86408a 100644 --- a/pkg/flags/flags.go +++ b/pkg/flags/flags.go @@ -13,6 +13,7 @@ type CommonFlags struct { IdpAccount string IdpProvider string MFA string + MFAIPAddress string MFAToken string URL string Username string @@ -38,6 +39,7 @@ type CommonFlags struct { // LoginExecFlags flags for the Login / Exec commands type LoginExecFlags struct { CommonFlags *CommonFlags + DownloadBrowser bool Force bool DuoMFAOption string ExecProfile string @@ -75,6 +77,10 @@ func ApplyFlagOverrides(commonFlags *CommonFlags, account *cfg.IDPAccount) { account.MFA = commonFlags.MFA } + if commonFlags.MFAIPAddress != "" { + account.MFAIPAddress = commonFlags.MFAIPAddress + } + if commonFlags.AmazonWebservicesURN != "" { account.AmazonWebservicesURN = commonFlags.AmazonWebservicesURN } diff --git a/pkg/page/form_test.go b/pkg/page/form_test.go index 8b0656edc..00bfcdf3a 100644 --- a/pkg/page/form_test.go +++ b/pkg/page/form_test.go @@ -2,8 +2,8 @@ package page import ( "bytes" - "io/ioutil" "net/url" + "os" "testing" "github.com/PuerkitoBio/goquery" @@ -11,7 +11,7 @@ import ( ) func TestNewFormFromDocument(t *testing.T) { - data, err := ioutil.ReadFile("example/multi-form.html") + data, err := os.ReadFile("example/multi-form.html") require.Nil(t, err) doc, err := goquery.NewDocumentFromReader(bytes.NewReader(data)) diff --git a/pkg/provider/aad/aad.go b/pkg/provider/aad/aad.go index 0c2607097..0bfb8121f 100644 --- a/pkg/provider/aad/aad.go +++ b/pkg/provider/aad/aad.go @@ -6,7 +6,6 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "log" "net/http" "net/url" @@ -15,13 +14,16 @@ import ( "github.com/PuerkitoBio/goquery" "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/versent/saml2aws/v2/pkg/cfg" "github.com/versent/saml2aws/v2/pkg/creds" "github.com/versent/saml2aws/v2/pkg/prompter" "github.com/versent/saml2aws/v2/pkg/provider" - "golang.org/x/net/html" ) +var logger = logrus.WithField("provider", "AzureAD") + // Client wrapper around AzureAD enabling authentication and retrieval of assertions type Client struct { provider.ValidateBase @@ -30,440 +32,76 @@ type Client struct { idpAccount *cfg.IDPAccount } -// Autogenerate startSAML Response struct -// some case, some fields is not exists -type startSAMLResponse struct { - FShowPersistentCookiesWarning bool `json:"fShowPersistentCookiesWarning"` - URLMsaLogout string `json:"urlMsaLogout"` - ShowCantAccessAccountLink bool `json:"showCantAccessAccountLink"` - URLGitHubFed string `json:"urlGitHubFed"` - FShowSignInWithGitHubOnlyOnCredPicker bool `json:"fShowSignInWithGitHubOnlyOnCredPicker"` - FEnableShowResendCode bool `json:"fEnableShowResendCode"` - IShowResendCodeDelay int `json:"iShowResendCodeDelay"` - SSMSCtryPhoneData string `json:"sSMSCtryPhoneData"` - FUseInlinePhoneNumber bool `json:"fUseInlinePhoneNumber"` - URLSessionState string `json:"urlSessionState"` - URLResetPassword string `json:"urlResetPassword"` - URLMsaResetPassword string `json:"urlMsaResetPassword"` - URLLogin string `json:"urlLogin"` - URLSignUp string `json:"urlSignUp"` - URLGetCredentialType string `json:"urlGetCredentialType"` - URLGetOneTimeCode string `json:"urlGetOneTimeCode"` - URLLogout string `json:"urlLogout"` - URLForget string `json:"urlForget"` - URLDisambigRename string `json:"urlDisambigRename"` - URLGoToAADError string `json:"urlGoToAADError"` - URLDssoStatus string `json:"urlDssoStatus"` - URLFidoHelp string `json:"urlFidoHelp"` - URLFidoLogin string `json:"urlFidoLogin"` - URLPostAad string `json:"urlPostAad"` - URLPostMsa string `json:"urlPostMsa"` - URLPIAEndAuth string `json:"urlPIAEndAuth"` - FCBShowSignUp bool `json:"fCBShowSignUp"` - FKMSIEnabled bool `json:"fKMSIEnabled"` - ILoginMode int `json:"iLoginMode"` - FAllowPhoneSignIn bool `json:"fAllowPhoneSignIn"` - FAllowPhoneInput bool `json:"fAllowPhoneInput"` - FAllowSkypeNameLogin bool `json:"fAllowSkypeNameLogin"` - IMaxPollErrors int `json:"iMaxPollErrors"` - IPollingTimeout int `json:"iPollingTimeout"` - SrsSuccess bool `json:"srsSuccess"` - FShowSwitchUser bool `json:"fShowSwitchUser"` - ArrValErrs []string `json:"arrValErrs"` - SErrorCode string `json:"sErrorCode"` - SErrTxt string `json:"sErrTxt"` - SResetPasswordPrefillParam string `json:"sResetPasswordPrefillParam"` - OnPremPasswordValidationConfig struct { - IsUserRealmPrecheckEnabled bool `json:"isUserRealmPrecheckEnabled"` - } `json:"onPremPasswordValidationConfig"` - FSwitchDisambig bool `json:"fSwitchDisambig"` - OCancelPostParams struct { - Error string `json:"error"` - ErrorSubcode string `json:"error_subcode"` - State string `json:"state"` - } `json:"oCancelPostParams"` - IAllowedIdentities int `json:"iAllowedIdentities"` - IRemoteNgcPollingType int `json:"iRemoteNgcPollingType"` - IsGlobalTenant bool `json:"isGlobalTenant"` - FIsFidoSupported bool `json:"fIsFidoSupported"` - FUseNewNoPasswordTypes bool `json:"fUseNewNoPasswordTypes"` - IMaxStackForKnockoutAsyncComponents int `json:"iMaxStackForKnockoutAsyncComponents"` - StrCopyrightTxt string `json:"strCopyrightTxt"` - FShowButtons bool `json:"fShowButtons"` - URLCdn string `json:"urlCdn"` - URLFooterTOU string `json:"urlFooterTOU"` - URLFooterPrivacy string `json:"urlFooterPrivacy"` - URLPost string `json:"urlPost"` - URLRefresh string `json:"urlRefresh"` - URLCancel string `json:"urlCancel"` - IPawnIcon int `json:"iPawnIcon"` - IPollingInterval int `json:"iPollingInterval"` - SPOSTUsername string `json:"sPOST_Username"` - SFT string `json:"sFT"` - SFTName string `json:"sFTName"` - SSessionIdentifierName string `json:"sSessionIdentifierName"` - SCtx string `json:"sCtx"` - IProductIcon int `json:"iProductIcon"` - URLReportPageLoad string `json:"urlReportPageLoad"` - StaticTenantBranding interface{} `json:"staticTenantBranding"` - OAppCobranding struct { - } `json:"oAppCobranding"` - IBackgroundImage int `json:"iBackgroundImage"` - ArrSessions []interface{} `json:"arrSessions"` - FUseConstantPolling bool `json:"fUseConstantPolling"` - FUseFlowTokenAsCanary bool `json:"fUseFlowTokenAsCanary"` - FApplicationInsightsEnabled bool `json:"fApplicationInsightsEnabled"` - IApplicationInsightsEnabledPercentage int `json:"iApplicationInsightsEnabledPercentage"` - URLSetDebugMode string `json:"urlSetDebugMode"` - FEnableCSSAnimation bool `json:"fEnableCssAnimation"` - FAllowGrayOutLightBox bool `json:"fAllowGrayOutLightBox"` - FIsRemoteNGCSupported bool `json:"fIsRemoteNGCSupported"` - Scid int `json:"scid"` - Hpgact int `json:"hpgact"` - Hpgid int `json:"hpgid"` - Pgid string `json:"pgid"` - APICanary string `json:"apiCanary"` - Canary string `json:"canary"` - CorrelationID string `json:"correlationId"` - SessionID string `json:"sessionId"` - Locale struct { - Mkt string `json:"mkt"` - Lcid int `json:"lcid"` - } `json:"locale"` - SlMaxRetry int `json:"slMaxRetry"` - SlReportFailure bool `json:"slReportFailure"` - Strings struct { - Desktopsso struct { - Authenticatingmessage string `json:"authenticatingmessage"` - } `json:"desktopsso"` - } `json:"strings"` - Enums struct { - ClientMetricsModes struct { - None int `json:"None"` - SubmitOnPost int `json:"SubmitOnPost"` - SubmitOnRedirect int `json:"SubmitOnRedirect"` - InstrumentPlt int `json:"InstrumentPlt"` - } `json:"ClientMetricsModes"` - } `json:"enums"` - Urls struct { - Instr struct { - Pageload string `json:"pageload"` - Dssostatus string `json:"dssostatus"` - } `json:"instr"` - } `json:"urls"` - Browser struct { - Ltr int `json:"ltr"` - Other int `json:"_Other"` - Full int `json:"Full"` - REOther int `json:"RE_Other"` - B struct { - Name string `json:"name"` - Major int `json:"major"` - Minor int `json:"minor"` - } `json:"b"` - Os struct { - Name string `json:"name"` - Version string `json:"version"` - } `json:"os"` - V int `json:"V"` - } `json:"browser"` - Watson struct { - URL string `json:"url"` - Bundle string `json:"bundle"` - Sbundle string `json:"sbundle"` - Fbundle string `json:"fbundle"` - ResetErrorPeriod int `json:"resetErrorPeriod"` - MaxCorsErrors int `json:"maxCorsErrors"` - MaxInjectErrors int `json:"maxInjectErrors"` - MaxErrors int `json:"maxErrors"` - MaxTotalErrors int `json:"maxTotalErrors"` - ExpSrcs []string `json:"expSrcs"` - EnvErrorRedirect bool `json:"envErrorRedirect"` - EnvErrorURL string `json:"envErrorUrl"` - } `json:"watson"` - Loader struct { - CdnRoots []string `json:"cdnRoots"` - } `json:"loader"` - ServerDetails struct { - Slc string `json:"slc"` - Dc string `json:"dc"` - Ri string `json:"ri"` - Ver struct { - V []int `json:"v"` - } `json:"ver"` - Rt string `json:"rt"` - Et int `json:"et"` - } `json:"serverDetails"` - Country string `json:"country"` - FBreakBrandingSigninString bool `json:"fBreakBrandingSigninString"` - Bsso struct { - Type string `json:"type"` - Reason string `json:"reason"` - } `json:"bsso"` - URLNoCookies string `json:"urlNoCookies"` - FTrimChromeBssoURL bool `json:"fTrimChromeBssoUrl"` +// Autogenrated Converged Response struct +// for some cases, some fields may not exist +type ConvergedResponse struct { + URLGetCredentialType string `json:"urlGetCredentialType"` + ArrUserProofs []userProof `json:"arrUserProofs"` + URLSkipMfaRegistration string `json:"urlSkipMfaRegistration"` + OPerAuthPollingInterval map[string]float64 `json:"oPerAuthPollingInterval"` + URLBeginAuth string `json:"urlBeginAuth"` + URLEndAuth string `json:"urlEndAuth"` + URLPost string `json:"urlPost"` + SErrorCode string `json:"sErrorCode"` + SErrTxt string `json:"sErrTxt"` + SPOSTUsername string `json:"sPOST_Username"` + SFT string `json:"sFT"` + SFTName string `json:"sFTName"` + SCtx string `json:"sCtx"` + Hpgact int `json:"hpgact"` + Hpgid int `json:"hpgid"` + Pgid string `json:"pgid"` + APICanary string `json:"apiCanary"` + Canary string `json:"canary"` + CorrelationID string `json:"correlationId"` + SessionID string `json:"sessionId"` } -// Autogenerate password login response -// some case, some fields is not exists -type passwordLoginResponse struct { - ArrUserProofs []userProof `json:"arrUserProofs"` - FHideIHaveCodeLink bool `json:"fHideIHaveCodeLink"` - OPerAuthPollingInterval map[string]float64 `json:"oPerAuthPollingInterval"` - FProofIndexedByType bool `json:"fProofIndexedByType"` - URLBeginAuth string `json:"urlBeginAuth"` - URLEndAuth string `json:"urlEndAuth"` - ISAMode int `json:"iSAMode"` - ITrustedDeviceCheckboxConfig int `json:"iTrustedDeviceCheckboxConfig"` - IMaxPollAttempts int `json:"iMaxPollAttempts"` - IPollingTimeout int `json:"iPollingTimeout"` - IPollingBackoffInterval float64 `json:"iPollingBackoffInterval"` - IRememberMfaDuration float64 `json:"iRememberMfaDuration"` - STrustedDeviceCheckboxName string `json:"sTrustedDeviceCheckboxName"` - SAuthMethodInputFieldName string `json:"sAuthMethodInputFieldName"` - ISAOtcLength int `json:"iSAOtcLength"` - ITotpOtcLength int `json:"iTotpOtcLength"` - URLMoreInfo string `json:"urlMoreInfo"` - FShowViewDetailsLink bool `json:"fShowViewDetailsLink"` - FAlwaysUpdateFTInSasEnd bool `json:"fAlwaysUpdateFTInSasEnd"` - IMaxStackForKnockoutAsyncComponents int `json:"iMaxStackForKnockoutAsyncComponents"` - StrCopyrightTxt string `json:"strCopyrightTxt"` - FShowButtons bool `json:"fShowButtons"` - URLCdn string `json:"urlCdn"` - URLFooterTOU string `json:"urlFooterTOU"` - URLFooterPrivacy string `json:"urlFooterPrivacy"` - URLPost string `json:"urlPost"` - URLCancel string `json:"urlCancel"` - IPawnIcon int `json:"iPawnIcon"` - IPollingInterval int `json:"iPollingInterval"` - SPOSTUsername string `json:"sPOST_Username"` - SFT string `json:"sFT"` - SFTName string `json:"sFTName"` - SCtx string `json:"sCtx"` - DynamicTenantBranding []struct { - Locale int `json:"Locale"` - Illustration string `json:"Illustration"` - UserIDLabel string `json:"UserIdLabel"` - KeepMeSignedInDisabled bool `json:"KeepMeSignedInDisabled"` - UseTransparentLightBox bool `json:"UseTransparentLightBox"` - } `json:"dynamicTenantBranding"` - OAppCobranding struct { - } `json:"oAppCobranding"` - IBackgroundImage int `json:"iBackgroundImage"` - FUseConstantPolling bool `json:"fUseConstantPolling"` - FUseFlowTokenAsCanary bool `json:"fUseFlowTokenAsCanary"` - FApplicationInsightsEnabled bool `json:"fApplicationInsightsEnabled"` - IApplicationInsightsEnabledPercentage int `json:"iApplicationInsightsEnabledPercentage"` - URLSetDebugMode string `json:"urlSetDebugMode"` - FEnableCSSAnimation bool `json:"fEnableCssAnimation"` - FAllowGrayOutLightBox bool `json:"fAllowGrayOutLightBox"` - FIsRemoteNGCSupported bool `json:"fIsRemoteNGCSupported"` - Scid int `json:"scid"` - Hpgact int `json:"hpgact"` - Hpgid int `json:"hpgid"` - Pgid string `json:"pgid"` - APICanary string `json:"apiCanary"` - Canary string `json:"canary"` - CorrelationID string `json:"correlationId"` - SessionID string `json:"sessionId"` - Locale struct { - Mkt string `json:"mkt"` - Lcid int `json:"lcid"` - } `json:"locale"` - SlMaxRetry int `json:"slMaxRetry"` - SlReportFailure bool `json:"slReportFailure"` - Strings struct { - Desktopsso struct { - Authenticatingmessage string `json:"authenticatingmessage"` - } `json:"desktopsso"` - } `json:"strings"` - Enums struct { - ClientMetricsModes struct { - None int `json:"None"` - SubmitOnPost int `json:"SubmitOnPost"` - SubmitOnRedirect int `json:"SubmitOnRedirect"` - InstrumentPlt int `json:"InstrumentPlt"` - } `json:"ClientMetricsModes"` - } `json:"enums"` - Urls struct { - Instr struct { - Pageload string `json:"pageload"` - Dssostatus string `json:"dssostatus"` - } `json:"instr"` - } `json:"urls"` - Browser struct { - Ltr int `json:"ltr"` - Other int `json:"_Other"` - Full int `json:"Full"` - REOther int `json:"RE_Other"` - B struct { - Name string `json:"name"` - Major int `json:"major"` - Minor int `json:"minor"` - } `json:"b"` - Os struct { - Name string `json:"name"` - Version string `json:"version"` - } `json:"os"` - V int `json:"V"` - } `json:"browser"` - Watson struct { - URL string `json:"url"` - Bundle string `json:"bundle"` - Sbundle string `json:"sbundle"` - Fbundle string `json:"fbundle"` - ResetErrorPeriod int `json:"resetErrorPeriod"` - MaxCorsErrors int `json:"maxCorsErrors"` - MaxInjectErrors int `json:"maxInjectErrors"` - MaxErrors int `json:"maxErrors"` - MaxTotalErrors int `json:"maxTotalErrors"` - ExpSrcs []string `json:"expSrcs"` - EnvErrorRedirect bool `json:"envErrorRedirect"` - EnvErrorURL string `json:"envErrorUrl"` - } `json:"watson"` - Loader struct { - CdnRoots []string `json:"cdnRoots"` - } `json:"loader"` - ServerDetails struct { - Slc string `json:"slc"` - Dc string `json:"dc"` - Ri string `json:"ri"` - Ver struct { - V []int `json:"v"` - } `json:"ver"` - Rt string `json:"rt"` - Et int `json:"et"` - } `json:"serverDetails"` - Country string `json:"country"` - FBreakBrandingSigninString bool `json:"fBreakBrandingSigninString"` - URLNoCookies string `json:"urlNoCookies"` - FTrimChromeBssoURL bool `json:"fTrimChromeBssoUrl"` +// Autogenerated GetCredentialType Request struct +// for some cases, some fields may not exist +type GetCredentialTypeRequest struct { + Username string `json:"username"` + IsOtherIdpSupported bool `json:"isOtherIdpSupported"` + CheckPhones bool `json:"checkPhones"` + IsRemoteNGCSupported bool `json:"isRemoteNGCSupported"` + IsCookieBannerShown bool `json:"isCookieBannerShown"` + IsFidoSupported bool `json:"isFidoSupported"` + OriginalRequest string `json:"originalRequest"` + Country string `json:"country"` + Forceotclogin bool `json:"forceotclogin"` + IsExternalFederationDisallowed bool `json:"isExternalFederationDisallowed"` + IsRemoteConnectSupported bool `json:"isRemoteConnectSupported"` + FederationFlags int `json:"federationFlags"` + IsSignup bool `json:"isSignup"` + FlowToken string `json:"flowToken"` + IsAccessPassSupported bool `json:"isAccessPassSupported"` } -// Autogenerated skip mfa login response -type SkipMfaResponse struct { - URLPostRedirect string `json:"urlPostRedirect"` - URLSkipMfaRegistration string `json:"urlSkipMfaRegistration"` - URLMoreInfo string `json:"urlMoreInfo"` - SProofUpToken string `json:"sProofUpToken"` - SProofUpTokenName string `json:"sProofUpTokenName"` - SProofUpAuthState string `json:"sProofUpAuthState"` - SCanaryToken string `json:"sCanaryToken"` - IRemainingDaysToSkipMfaRegistration int `json:"iRemainingDaysToSkipMfaRegistration"` - IMaxStackForKnockoutAsyncComponents int `json:"iMaxStackForKnockoutAsyncComponents"` - StrCopyrightTxt string `json:"strCopyrightTxt"` - FShowButtons bool `json:"fShowButtons"` - URLCdn string `json:"urlCdn"` - URLFooterTOU string `json:"urlFooterTOU"` - URLFooterPrivacy string `json:"urlFooterPrivacy"` - URLPost string `json:"urlPost"` - URLCancel string `json:"urlCancel"` - IPawnIcon int `json:"iPawnIcon"` - SPOSTUsername string `json:"sPOST_Username"` - SFT string `json:"sFT"` - SFTName string `json:"sFTName"` - SCanaryTokenName string `json:"sCanaryTokenName"` - DynamicTenantBranding []struct { - Locale int `json:"Locale"` - Illustration string `json:"Illustration"` - UserIDLabel string `json:"UserIdLabel"` - KeepMeSignedInDisabled bool `json:"KeepMeSignedInDisabled"` - UseTransparentLightBox bool `json:"UseTransparentLightBox"` - } `json:"dynamicTenantBranding"` - OAppCobranding struct { - } `json:"oAppCobranding"` - IBackgroundImage int `json:"iBackgroundImage"` - FUseConstantPolling bool `json:"fUseConstantPolling"` - FUseFlowTokenAsCanary bool `json:"fUseFlowTokenAsCanary"` - FApplicationInsightsEnabled bool `json:"fApplicationInsightsEnabled"` - IApplicationInsightsEnabledPercentage int `json:"iApplicationInsightsEnabledPercentage"` - URLSetDebugMode string `json:"urlSetDebugMode"` - FEnableCSSAnimation bool `json:"fEnableCssAnimation"` - FAllowGrayOutLightBox bool `json:"fAllowGrayOutLightBox"` - FIsRemoteNGCSupported bool `json:"fIsRemoteNGCSupported"` - Scid int `json:"scid"` - Hpgact int `json:"hpgact"` - Hpgid int `json:"hpgid"` - Pgid string `json:"pgid"` - APICanary string `json:"apiCanary"` - Canary string `json:"canary"` - CorrelationID string `json:"correlationId"` - SessionID string `json:"sessionId"` - Locale struct { - Mkt string `json:"mkt"` - Lcid int `json:"lcid"` - } `json:"locale"` - SlMaxRetry int `json:"slMaxRetry"` - SlReportFailure bool `json:"slReportFailure"` - Strings struct { - Desktopsso struct { - Authenticatingmessage string `json:"authenticatingmessage"` - } `json:"desktopsso"` - } `json:"strings"` - Enums struct { - ClientMetricsModes struct { - None int `json:"None"` - SubmitOnPost int `json:"SubmitOnPost"` - SubmitOnRedirect int `json:"SubmitOnRedirect"` - InstrumentPlt int `json:"InstrumentPlt"` - } `json:"ClientMetricsModes"` - } `json:"enums"` - Urls struct { - Instr struct { - Pageload string `json:"pageload"` - Dssostatus string `json:"dssostatus"` - } `json:"instr"` - } `json:"urls"` - Browser struct { - Ltr int `json:"ltr"` - Other int `json:"_Other"` - Full int `json:"Full"` - REOther int `json:"RE_Other"` - B struct { - Name string `json:"name"` - Major int `json:"major"` - Minor int `json:"minor"` - } `json:"b"` - Os struct { - Name string `json:"name"` - Version string `json:"version"` - } `json:"os"` - V int `json:"V"` - } `json:"browser"` - Watson struct { - URL string `json:"url"` - Bundle string `json:"bundle"` - Sbundle string `json:"sbundle"` - Fbundle string `json:"fbundle"` - ResetErrorPeriod int `json:"resetErrorPeriod"` - MaxCorsErrors int `json:"maxCorsErrors"` - MaxInjectErrors int `json:"maxInjectErrors"` - MaxErrors int `json:"maxErrors"` - MaxTotalErrors int `json:"maxTotalErrors"` - ExpSrcs []string `json:"expSrcs"` - EnvErrorRedirect bool `json:"envErrorRedirect"` - EnvErrorURL string `json:"envErrorUrl"` - } `json:"watson"` - Loader struct { - CdnRoots []string `json:"cdnRoots"` - } `json:"loader"` - ServerDetails struct { - Slc string `json:"slc"` - Dc string `json:"dc"` - Ri string `json:"ri"` - Ver struct { - V []int `json:"v"` - } `json:"ver"` - Rt string `json:"rt"` - Et int `json:"et"` - } `json:"serverDetails"` - Country string `json:"country"` - FBreakBrandingSigninString bool `json:"fBreakBrandingSigninString"` - URLNoCookies string `json:"urlNoCookies"` - FTrimChromeBssoURL bool `json:"fTrimChromeBssoUrl"` +// Autogenerated GetCredentialType Response struct +// for some cases, some fields may not exist +type GetCredentialTypeResponse struct { + Username string `json:"Username"` + Display string `json:"Display"` + IfExistsResult int `json:"IfExistsResult"` + IsUnmanaged bool `json:"IsUnmanaged"` + ThrottleStatus int `json:"ThrottleStatus"` + Credentials struct { + PrefCredential int `json:"PrefCredential"` + HasPassword bool `json:"HasPassword"` + RemoteNgcParams interface{} `json:"RemoteNgcParams"` + FidoParams interface{} `json:"FidoParams"` + SasParams interface{} `json:"SasParams"` + CertAuthParams interface{} `json:"CertAuthParams"` + GoogleParams interface{} `json:"GoogleParams"` + FacebookParams interface{} `json:"FacebookParams"` + FederationRedirectURL string `json:"FederationRedirectUrl"` + } `json:"Credentials"` + FlowToken string `json:"FlowToken"` + IsSignupDisallowed bool `json:"IsSignupDisallowed"` + APICanary string `json:"apiCanary"` } -// mfa request +// MFA Request struct type mfaRequest struct { AuthMethodID string `json:"AuthMethodId"` Method string `json:"Method"` @@ -473,7 +111,7 @@ type mfaRequest struct { AdditionalAuthData string `json:"AdditionalAuthData,omitempty"` } -// mfa response +// MFA Response struct type mfaResponse struct { Success bool `json:"Success"` ResultValue string `json:"ResultValue"` @@ -486,122 +124,7 @@ type mfaResponse struct { SessionID string `json:"SessionId"` CorrelationID string `json:"CorrelationId"` Timestamp time.Time `json:"Timestamp"` -} - -// Autogenerate ProcessAuth response -// some case, some fields is not exists -type processAuthResponse struct { - IMaxStackForKnockoutAsyncComponents int `json:"iMaxStackForKnockoutAsyncComponents"` - StrCopyrightTxt string `json:"strCopyrightTxt"` - FShowButtons bool `json:"fShowButtons"` - URLCdn string `json:"urlCdn"` - URLFooterTOU string `json:"urlFooterTOU"` - URLFooterPrivacy string `json:"urlFooterPrivacy"` - URLPost string `json:"urlPost"` - IPawnIcon int `json:"iPawnIcon"` - SPOSTUsername string `json:"sPOST_Username"` - SFT string `json:"sFT"` - SFTName string `json:"sFTName"` - SCtx string `json:"sCtx"` - SCanaryTokenName string `json:"sCanaryTokenName"` - DynamicTenantBranding []struct { - Locale int `json:"Locale"` - Illustration string `json:"Illustration"` - UserIDLabel string `json:"UserIdLabel"` - KeepMeSignedInDisabled bool `json:"KeepMeSignedInDisabled"` - UseTransparentLightBox bool `json:"UseTransparentLightBox"` - } `json:"dynamicTenantBranding"` - OAppCobranding struct { - } `json:"oAppCobranding"` - IBackgroundImage int `json:"iBackgroundImage"` - FUseConstantPolling bool `json:"fUseConstantPolling"` - FUseFlowTokenAsCanary bool `json:"fUseFlowTokenAsCanary"` - FApplicationInsightsEnabled bool `json:"fApplicationInsightsEnabled"` - IApplicationInsightsEnabledPercentage int `json:"iApplicationInsightsEnabledPercentage"` - URLSetDebugMode string `json:"urlSetDebugMode"` - FEnableCSSAnimation bool `json:"fEnableCssAnimation"` - FAllowGrayOutLightBox bool `json:"fAllowGrayOutLightBox"` - FIsRemoteNGCSupported bool `json:"fIsRemoteNGCSupported"` - Scid int `json:"scid"` - Hpgact int `json:"hpgact"` - Hpgid int `json:"hpgid"` - Pgid string `json:"pgid"` - APICanary string `json:"apiCanary"` - Canary string `json:"canary"` - CorrelationID string `json:"correlationId"` - SessionID string `json:"sessionId"` - Locale struct { - Mkt string `json:"mkt"` - Lcid int `json:"lcid"` - } `json:"locale"` - SlMaxRetry int `json:"slMaxRetry"` - SlReportFailure bool `json:"slReportFailure"` - Strings struct { - Desktopsso struct { - Authenticatingmessage string `json:"authenticatingmessage"` - } `json:"desktopsso"` - } `json:"strings"` - Enums struct { - ClientMetricsModes struct { - None int `json:"None"` - SubmitOnPost int `json:"SubmitOnPost"` - SubmitOnRedirect int `json:"SubmitOnRedirect"` - InstrumentPlt int `json:"InstrumentPlt"` - } `json:"ClientMetricsModes"` - } `json:"enums"` - Urls struct { - Instr struct { - Pageload string `json:"pageload"` - Dssostatus string `json:"dssostatus"` - } `json:"instr"` - } `json:"urls"` - Browser struct { - Ltr int `json:"ltr"` - Other int `json:"_Other"` - Full int `json:"Full"` - REOther int `json:"RE_Other"` - B struct { - Name string `json:"name"` - Major int `json:"major"` - Minor int `json:"minor"` - } `json:"b"` - Os struct { - Name string `json:"name"` - Version string `json:"version"` - } `json:"os"` - V int `json:"V"` - } `json:"browser"` - Watson struct { - URL string `json:"url"` - Bundle string `json:"bundle"` - Sbundle string `json:"sbundle"` - Fbundle string `json:"fbundle"` - ResetErrorPeriod int `json:"resetErrorPeriod"` - MaxCorsErrors int `json:"maxCorsErrors"` - MaxInjectErrors int `json:"maxInjectErrors"` - MaxErrors int `json:"maxErrors"` - MaxTotalErrors int `json:"maxTotalErrors"` - ExpSrcs []string `json:"expSrcs"` - EnvErrorRedirect bool `json:"envErrorRedirect"` - EnvErrorURL string `json:"envErrorUrl"` - } `json:"watson"` - Loader struct { - CdnRoots []string `json:"cdnRoots"` - } `json:"loader"` - ServerDetails struct { - Slc string `json:"slc"` - Dc string `json:"dc"` - Ri string `json:"ri"` - Ver struct { - V []int `json:"v"` - } `json:"ver"` - Rt string `json:"rt"` - Et int `json:"et"` - } `json:"serverDetails"` - Country string `json:"country"` - FBreakBrandingSigninString bool `json:"fBreakBrandingSigninString"` - URLNoCookies string `json:"urlNoCookies"` - FTrimChromeBssoURL bool `json:"fTrimChromeBssoUrl"` + Entropy int `json:"Entropy"` } // A given method for a user to prove their indentity @@ -633,337 +156,299 @@ func New(idpAccount *cfg.IDPAccount) (*Client, error) { // Authenticate to AzureAD and return the data from the body of the SAML assertion. func (ac *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) { - var samlAssertion string var res *http.Response + var err error + var resBody []byte + var resBodyStr string + var convergedResponse *ConvergedResponse // idpAccount.URL = https://account.activedirectory.windowsazure.com // startSAML startURL := fmt.Sprintf("%s/applications/redirecttofederatedapplication.aspx?Operation=LinkedSignIn&applicationId=%s", ac.idpAccount.URL, ac.idpAccount.AppID) - res, err := ac.client.Get(startURL) + res, err = ac.client.Get(startURL) if err != nil { - return samlAssertion, errors.Wrap(err, "error retrieving form") + return samlAssertion, errors.Wrap(err, "error retrieving entry URL") + } + +AuthProcessor: + for { + resBody, _ = io.ReadAll(res.Body) + resBodyStr = string(resBody) + // reset res.Body so it can be read again later if required + res.Body = io.NopCloser(bytes.NewBuffer(resBody)) + + switch { + case strings.Contains(resBodyStr, "ConvergedSignIn"): + logger.Debug("processing ConvergedSignIn") + res, err = ac.processConvergedSignIn(res, resBodyStr, loginDetails) + case strings.Contains(resBodyStr, "ConvergedProofUpRedirect"): + logger.Debug("processing ConvergedProofUpRedirect") + res, err = ac.processConvergedProofUpRedirect(res, resBodyStr) + case strings.Contains(resBodyStr, "KmsiInterrupt"): + logger.Debug("processing KmsiInterrupt") + res, err = ac.processKmsiInterrupt(res, resBodyStr) + case strings.Contains(resBodyStr, "ConvergedTFA"): + logger.Debug("processing ConvergedTFA") + res, err = ac.processConvergedTFA(res, resBodyStr) + case strings.Contains(resBodyStr, "SAMLRequest"): + logger.Debug("processing SAMLRequest") + res, err = ac.processSAMLRequest(res, resBodyStr) + case ac.isHiddenForm(resBodyStr): + if samlAssertion, _ = ac.getSamlAssertion(resBodyStr); samlAssertion != "" { + logger.Debug("processing a SAMLResponse") + return samlAssertion, nil + } + logger.Debug("processing a 'hiddenform'") + res, err = ac.reProcessForm(resBodyStr) + default: + if strings.Contains(resBodyStr, "$Config") { + if err := ac.unmarshalEmbeddedJson(resBodyStr, &convergedResponse); err != nil { + return samlAssertion, errors.Wrap(err, "unmarshal error") + } + logger.Debug("unknown process step found:", convergedResponse.Pgid) + } else { + logger.Debug("reached an unknown page within the authentication process") + } + break AuthProcessor + } + if err != nil { + return samlAssertion, err + } } - // data is embedded javascript object - // - */ - isEnabledConditonalAccess := strings.HasPrefix(resBodyStr, "Working...") && strings.Contains(resBodyStr, "name=\"flowtoken\"") - - if isSkippedMFA || isEnabledConditonalAccess { - // require reprocess - if strings.Contains(resBodyStr, " - var loginPasswordJson string - if strings.Contains(resBodyStr, "$Config") { - loginPasswordJson = ac.getJsonFromConfig(resBodyStr) + if federationRedirectURL != "" { + res, err = ac.processADFSAuthentication(federationRedirectURL, loginDetails) + if err != nil { + return res, err } - resBodyStr, err = ac.processAuth(loginPasswordJson, res) + } else { + res, err = ac.processAuthentication(loginRequestUrl, refererUrl, loginDetails, convergedResponse) if err != nil { - return samlAssertion, err + return res, err } } - node, _ := html.Parse(strings.NewReader(resBodyStr)) - doc := goquery.NewDocumentFromNode(node) - - // data in input tag - authForm := url.Values{} - var authSubmitURL string - - doc.Find("input").Each(func(i int, s *goquery.Selection) { - name, ok := s.Attr("name") - if !ok { - return - } - value, ok := s.Attr("value") - if !ok { - return - } - authForm.Set(name, value) - }) - - doc.Find("form").Each(func(i int, s *goquery.Selection) { - action, ok := s.Attr("action") - if !ok { - return - } - authSubmitURL = action - }) + return res, nil +} - if authSubmitURL == "" { - return samlAssertion, fmt.Errorf("unable to locate IDP oidc form submit URL") +func (ac *Client) requestGetCredentialType(refererUrl string, loginDetails *creds.LoginDetails, convergedResponse *ConvergedResponse) (GetCredentialTypeResponse, *http.Response, error) { + var res *http.Response + var getCredentialTypeResponse GetCredentialTypeResponse + + reqBodyObj := GetCredentialTypeRequest{ + Username: loginDetails.Username, + IsOtherIdpSupported: true, + CheckPhones: false, + IsRemoteNGCSupported: false, + IsCookieBannerShown: false, + IsFidoSupported: false, + OriginalRequest: convergedResponse.SCtx, + FlowToken: convergedResponse.SFT, + } + reqBodyJson, err := json.Marshal(reqBodyObj) + if err != nil { + return getCredentialTypeResponse, res, errors.Wrap(err, "failed to build GetCredentialType request JSON") } - req, err := http.NewRequest("POST", authSubmitURL, strings.NewReader(authForm.Encode())) + req, err := http.NewRequest("POST", convergedResponse.URLGetCredentialType, strings.NewReader(string(reqBodyJson))) if err != nil { - return samlAssertion, errors.Wrap(err, "error building authentication request") + return getCredentialTypeResponse, res, errors.Wrap(err, "error building GetCredentialType request") } - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + req.Header.Add("canary", convergedResponse.APICanary) + req.Header.Add("client-request-id", convergedResponse.CorrelationID) + req.Header.Add("hpgact", fmt.Sprint(convergedResponse.Hpgact)) + req.Header.Add("hpgid", fmt.Sprint(convergedResponse.Hpgid)) + req.Header.Add("hpgrequestid", convergedResponse.SessionID) + req.Header.Add("Referer", refererUrl) - ac.client.EnableFollowRedirect() res, err = ac.client.Do(req) if err != nil { - return samlAssertion, errors.Wrap(err, "error retrieving oidc login form results") + return getCredentialTypeResponse, res, errors.Wrap(err, "error retrieving GetCredentialType results") } - // get saml assertion - oidcResponse, err := ioutil.ReadAll(res.Body) + err = json.NewDecoder(res.Body).Decode(&getCredentialTypeResponse) if err != nil { - return samlAssertion, errors.Wrap(err, "oidc login response error") + return getCredentialTypeResponse, res, errors.Wrap(err, "error decoding GetCredentialType results") } - oidcResponseStr := string(oidcResponse) + return getCredentialTypeResponse, res, nil +} - // data is embedded javascript - // window.location = 'https:/..../?SAMLRequest=......' - oidcResponseList := strings.Split(oidcResponseStr, ";") - var SAMLRequestURL string - for _, v := range oidcResponseList { - if strings.Contains(v, "SAMLRequest") { - startURLPos := strings.Index(v, "https://") - endURLPos := strings.Index(v[startURLPos:], "'") - if endURLPos == -1 { - endURLPos = strings.Index(v[startURLPos:], "\"") - } - SAMLRequestURL = v[startURLPos : startURLPos+endURLPos] - } +func (ac *Client) processADFSAuthentication(federationUrl string, loginDetails *creds.LoginDetails) (*http.Response, error) { + var res *http.Response + var err error + var resBodyStr string + var formValues url.Values + var formSubmitUrl string + var req *http.Request + res, err = ac.client.Get(federationUrl) + if err != nil { + return res, errors.Wrap(err, "error retrieving ADFS url") } - if SAMLRequestURL == "" { - return samlAssertion, fmt.Errorf("unable to locate SAMLRequest URL") - } - req, err = http.NewRequest("GET", SAMLRequestURL, nil) + resBodyStr, _ = ac.responseBodyAsString(res.Body) + + formValues, formSubmitUrl, err = ac.reSubmitFormData(resBodyStr) if err != nil { - return samlAssertion, errors.Wrap(err, "error building get request") + return res, errors.Wrap(err, "failed to parse ADFS login form") } - res, err = ac.client.Do(req) - if err != nil { - return samlAssertion, errors.Wrap(err, "error retrieving oidc login form results") + if formSubmitUrl == "" { + return res, fmt.Errorf("unable to locate ADFS form submit URL") } - // if mfa skipped then get $Config and urlSkipMfaRegistration - // get urlSkipMfaRegistraition to return saml assertion - resBodyStr, err = ac.responseBodyAsString(res.Body) + formValues.Set("UserName", loginDetails.Username) + formValues.Set("Password", loginDetails.Password) + formValues.Set("AuthMethod", "FormsAuthentication") + + req, err = http.NewRequest("POST", ac.fullUrl(res, formSubmitUrl), strings.NewReader(formValues.Encode())) if err != nil { - return samlAssertion, errors.Wrap(err, "error oidc login response read") - } - if strings.Contains(resBodyStr, "arrUserProofs") { - // data is embedded javascript object - // + + + + + + + + + +
+

JavaScript required

+

JavaScript is required. This web browser does not support JavaScript or JavaScript in this web browser is not enabled.

+

To find out if your web browser supports JavaScript or to enable JavaScript, see web browser help.

+
+ +
+
+
+
+
+
+ +
+
+ +
+ + + +
+
Sign in
+ +
+
+ +
+ +
+
+ + +
+ +
+ + +
+ +
+ Sign in +
+
+ +
+ +
+
+ + + + +
+
+ +
+ introduction +
+ + +
+ +
+ +
+
+
+
+
+ +
+
+
+ + + + + + diff --git a/pkg/provider/aad/testdata/ADFStrust.html b/pkg/provider/aad/testdata/ADFStrust.html new file mode 100644 index 000000000..4819c5a5f --- /dev/null +++ b/pkg/provider/aad/testdata/ADFStrust.html @@ -0,0 +1 @@ +Working...
\ No newline at end of file diff --git a/pkg/provider/aad/testdata/BeginAuth.json b/pkg/provider/aad/testdata/BeginAuth.json new file mode 100644 index 000000000..bd38ad592 --- /dev/null +++ b/pkg/provider/aad/testdata/BeginAuth.json @@ -0,0 +1 @@ +{"Success":true,"ResultValue":"Success","Message":null,"AuthMethodId":"OneWaySMS","ErrCode":0,"Retry":false,"FlowToken":"{{.SFT}}","Ctx":"{{.Ctx}}","SessionId":"{{.SessionId}}","CorrelationId":"{{.ClientRequestId}}","Timestamp":"2020-01-01T00:00:00Z","Entropy":0} \ No newline at end of file diff --git a/pkg/provider/aad/testdata/ConvergedProofUpRedirect.html b/pkg/provider/aad/testdata/ConvergedProofUpRedirect.html new file mode 100644 index 000000000..045e1facd --- /dev/null +++ b/pkg/provider/aad/testdata/ConvergedProofUpRedirect.html @@ -0,0 +1,73 @@ + + + + + + + Sign in to your account + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pkg/provider/aad/testdata/ConvergedSignIn.html b/pkg/provider/aad/testdata/ConvergedSignIn.html new file mode 100644 index 000000000..5d1779633 --- /dev/null +++ b/pkg/provider/aad/testdata/ConvergedSignIn.html @@ -0,0 +1,75 @@ + + + + + + + Sign in to your account + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pkg/provider/aad/testdata/ConvergedTFA.html b/pkg/provider/aad/testdata/ConvergedTFA.html new file mode 100644 index 000000000..f55cb0400 --- /dev/null +++ b/pkg/provider/aad/testdata/ConvergedTFA.html @@ -0,0 +1,74 @@ + + + + + + + Sign in to your account + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pkg/provider/aad/testdata/EndAuth.json b/pkg/provider/aad/testdata/EndAuth.json new file mode 100644 index 000000000..bd38ad592 --- /dev/null +++ b/pkg/provider/aad/testdata/EndAuth.json @@ -0,0 +1 @@ +{"Success":true,"ResultValue":"Success","Message":null,"AuthMethodId":"OneWaySMS","ErrCode":0,"Retry":false,"FlowToken":"{{.SFT}}","Ctx":"{{.Ctx}}","SessionId":"{{.SessionId}}","CorrelationId":"{{.ClientRequestId}}","Timestamp":"2020-01-01T00:00:00Z","Entropy":0} \ No newline at end of file diff --git a/pkg/provider/aad/testdata/GetCredentialType_adfs.json b/pkg/provider/aad/testdata/GetCredentialType_adfs.json new file mode 100644 index 000000000..adadb936f --- /dev/null +++ b/pkg/provider/aad/testdata/GetCredentialType_adfs.json @@ -0,0 +1 @@ +{"Username":"{{.UserName}}","Display":"{{.UserName}}","IfExistsResult":0,"IsUnmanaged":false,"ThrottleStatus":1,"Credentials":{"PrefCredential":4,"HasPassword":true,"RemoteNgcParams":null,"FidoParams":null,"SasParams":null,"CertAuthParams":null,"GoogleParams":null,"FacebookParams":null,"FederationRedirectUrl":"{{.UrlFederationRedirect}}"},"EstsProperties":{"UserTenantBranding":[{"Locale":0,"BannerLogo":"https://via.placeholder.com/280x60.png","TileLogo":"https://via.placeholder.com/240x240.png","TileDarkLogo":"https://via.placeholder.com/240x240.png","UserIdLabel":"someone@example.com","KeepMeSignedInDisabled":false,"UseTransparentLightBox":false,"LayoutTemplateConfig":{"showHeader":false,"headerLogo":"","layoutType":0,"hideCantAccessYourAccount":false,"hideForgotMyPassword":false,"hideResetItNow":false,"hideAccountResetCredentials":false,"showFooter":true,"hideTOU":false,"hidePrivacy":false},"CustomizationFiles":{"strings":{"adminConsent":"","attributeCollection":"","authenticatorNudgeScreen":"","conditionalAccess":""},"customCssUrl":""}}],"DomainType":4},"FlowToken":"{{.SFT}}","IsSignupDisallowed":true,"apiCanary":"{{.ApiCanary}}"} \ No newline at end of file diff --git a/pkg/provider/aad/testdata/GetCredentialType_default.json b/pkg/provider/aad/testdata/GetCredentialType_default.json new file mode 100644 index 000000000..ef5811a92 --- /dev/null +++ b/pkg/provider/aad/testdata/GetCredentialType_default.json @@ -0,0 +1 @@ +{"Username":"{{.UserName}}","Display":"{{.UserName}}","IfExistsResult":0,"IsUnmanaged":false,"ThrottleStatus":0,"Credentials":{"PrefCredential":1,"HasPassword":true,"RemoteNgcParams":null,"FidoParams":null,"SasParams":null,"CertAuthParams":null,"GoogleParams":null,"FacebookParams":null},"EstsProperties":{"UserTenantBranding":null,"DomainType":3},"FlowToken":"{{.SFT}}","IsSignupDisallowed":true,"apiCanary":"{{.ApiCanary}}"} \ No newline at end of file diff --git a/pkg/provider/aad/testdata/HiddenForm.html b/pkg/provider/aad/testdata/HiddenForm.html new file mode 100644 index 000000000..b47150d3c --- /dev/null +++ b/pkg/provider/aad/testdata/HiddenForm.html @@ -0,0 +1 @@ +Working...
\ No newline at end of file diff --git a/pkg/provider/aad/testdata/KmsiInterrupt.html b/pkg/provider/aad/testdata/KmsiInterrupt.html new file mode 100644 index 000000000..a4eb86a1d --- /dev/null +++ b/pkg/provider/aad/testdata/KmsiInterrupt.html @@ -0,0 +1,73 @@ + + + + + + + Sign in to your account + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pkg/provider/aad/testdata/LoginEmbeddedJsonExtraJavascript.html b/pkg/provider/aad/testdata/LoginEmbeddedJsonExtraJavascript.html new file mode 100644 index 000000000..c3c6f5775 --- /dev/null +++ b/pkg/provider/aad/testdata/LoginEmbeddedJsonExtraJavascript.html @@ -0,0 +1,12 @@ + + + + + + + diff --git a/pkg/provider/aad/testdata/LoginEmbeddedJsonLineBreak.html b/pkg/provider/aad/testdata/LoginEmbeddedJsonLineBreak.html new file mode 100644 index 000000000..6b6795d05 --- /dev/null +++ b/pkg/provider/aad/testdata/LoginEmbeddedJsonLineBreak.html @@ -0,0 +1,12 @@ + + + + + + + diff --git a/pkg/provider/aad/testdata/LoginEmbeddedJsonNoLineBreak.html b/pkg/provider/aad/testdata/LoginEmbeddedJsonNoLineBreak.html new file mode 100644 index 000000000..b617f1f6e --- /dev/null +++ b/pkg/provider/aad/testdata/LoginEmbeddedJsonNoLineBreak.html @@ -0,0 +1,11 @@ + + + + + + + diff --git a/pkg/provider/aad/testdata/SAMLRequest.html b/pkg/provider/aad/testdata/SAMLRequest.html new file mode 100644 index 000000000..9d39c270b --- /dev/null +++ b/pkg/provider/aad/testdata/SAMLRequest.html @@ -0,0 +1,211 @@ + + + + + + + + + + + + +
+
+ + + +
+ + + + + + + + + + + + +
+ + + + +
+
+
+
+ +
+
+
+ +
+ + + + + +
+ +
+
+
+
+
+ +
+ +
+
+
+ + + + + + + + +
+ + + + diff --git a/pkg/provider/aad/testdata/SAMLResponse.html b/pkg/provider/aad/testdata/SAMLResponse.html new file mode 100644 index 000000000..be028ef01 --- /dev/null +++ b/pkg/provider/aad/testdata/SAMLResponse.html @@ -0,0 +1 @@ +Working...
\ No newline at end of file diff --git a/pkg/provider/aad/testdata/SAMLResponse.xml b/pkg/provider/aad/testdata/SAMLResponse.xml new file mode 100644 index 000000000..aaba12d27 --- /dev/null +++ b/pkg/provider/aad/testdata/SAMLResponse.xml @@ -0,0 +1,94 @@ + + https://sts.windows.net/25f4519b-eca5-405d-b516-123af862c268/ + + + + + + https://sts.windows.net/25f4519b-eca5-405d-b516-123af862c268/ + + + + + + + + + + + ba0vLeerzPU5SlBzNMQ95WNLauGojdAZBDdCPMJmUNI= + + + QE4db1da3PKU583Q0mm9MRpLaogENs95eWSkc8RvtU3kYwOVHpFtcVyG0wti54sc72V7rWSr3UoIGysgx_-3UJch_oG1JJi7IdNLhbFBx-PVxtAKvIdMkSM8tXRLuEtkNUB760jQAmie43Che8j47JdyWp4nh19QTDHjpH2vW9zldp-mhlLtl_QQQ-lJPd-LWC3A4xS0a81fenApzq4KvOY4zghapNih_dZOH6OO_UBgq_fyZ-x7gDiHin4UeySsaHQEBPr_mx5t6ilteSjm3J6HKlVVw9HNhmgry80UJkuVZ-7nWfgaawNjHDtG2UXN9k5oT0hCokMG7SlcPVKLqA== + + + MIIC8DCCAdigAwIBAgIQV3utGUh+Q55I54g7Y8RkUjANBgkqhkiG9w0BAQsFADA0MTIwMAYDVQQDEylNaWNyb3NvZnQgQXp1cmUgRmVkZXJhdGVkIFNTTyBDZXJ0aWZpY2F0ZTAeFw0xOTA4MDgwNzA1MDBaFw0yMjA4MDgwNzAyMTlaMDQxMjAwBgNVBAMTKU1pY3Jvc29mdCBBenVyZSBGZWRlcmF0ZWQgU1NPIENlcnRpZmljYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApO5wQVjdzEx2/j5gNrUh94wpBni4wNmRI3tKoSWJpRjnnlhwHSiiAWo7KX924owbbc8m+aqZ/dt7gdyADl02dJN5vjYwHy0rJoitC6j9hVHd/Fz7QOOhlaLwtxKfp7bgzvLYw3/HsAFbnJxwQWdddiPm6+2b903tdUehV9lR7LgLwa8pYA8ybnV/8KrgB9zwDi8c+h0Od3+SLvheCagOLmPZBc3u2YkW6BRLt3HIdT75Rv5G81ak3yKdmpjelIgcj/39x/g5K4xTYYJz8x/a8xdy1tax46Vr0h7xfg3YkuYy/kcs6JGilQEVsA/NVmAGPl7W7uu03CCFsi5Xc8aIXwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBiVFSvDRTGqlgnBaQjrdaN3GanD+vggrz8rzm+ccdFi72xkRKMmAxePVcgYNZWl4pZgXitQOa9otE4gxzLFQEXpShj/xgomZ1orF5Fx2DIP/TtHn+6BGK4pi/QsSDWqOx33lDnPjXY6Ouiyz4GoY50l6UfXzwyCiYBoI/r0Paf5bLSF9gV0aJInFswG28lXDsUydXKsByrprqvYpWX6lplRf/SgCmCf8l9eApk+558cWtIlUn1mDzYxt8z7X8xhBYXyg6193wz4A2ULhfB7No/bO6WDlaaK2YN1VSpjRdwDKpKiR2yy3kJRJl1IO8szqIYPKcrdTwGBNRDix1UEwdR + + + + + exampleuser@exampledomain.com + + + + + + + https://signin.aws.amazon.com/saml + + + + + 25f4519b-eca5-405d-b516-123af862c268 + + + 5159e491-c7a1-4c67-bff9-666cdab9a60b + + + Doe, John + + + https://sts.windows.net/25f4519b-eca5-405d-b516-123af862c268/ + + + urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport + http://schemas.microsoft.com/claims/multipleauthn + + + arn:aws:iam::012345678901:role/example_role,arn:aws:iam::012345678901:saml-provider/EXAMPLE_PROVIDER + arn:aws:iam::123456789012:role/example_role,arn:aws:iam::123456789012:saml-provider/EXAMPLE_PROVIDER + + + John + + + Doe + + + john.doe@exampledomain.com + + + exampleuser@exampledomain.com + + + arn:aws:iam::012345678901:role/example_role,arn:aws:iam::012345678901:saml-provider/EXAMPLE_PROVIDER + arn:aws:iam::123456789012:role/example_role,arn:aws:iam::123456789012:saml-provider/EXAMPLE_PROVIDER + + + exampleuser@exampledomain.com + + + exampleuser + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport + + + + \ No newline at end of file diff --git a/pkg/provider/adfs/adfs.go b/pkg/provider/adfs/adfs.go index d39334368..a908ff8e7 100644 --- a/pkg/provider/adfs/adfs.go +++ b/pkg/provider/adfs/adfs.go @@ -86,6 +86,19 @@ func (ac *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) authSubmitURL = action }) + // Trim whitespace and discard empty values from Kmsi field + if val, ok := authForm["Kmsi"]; ok { + var trimmedKmsi []string + var t string + for _, s := range val { + t = strings.TrimSpace(s) + if len(t) > 0 { + trimmedKmsi = append(trimmedKmsi, t) + } + } + authForm["Kmsi"] = trimmedKmsi + } + if authSubmitURL == "" { return samlAssertion, fmt.Errorf("unable to locate IDP authentication form submit URL") } else if strings.HasPrefix(authSubmitURL, "/") { @@ -135,7 +148,15 @@ func (ac *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) doc.Find("input").Each(func(i int, s *goquery.Selection) { updatePassthroughFormData(azureForm, s) }) - sel := doc.Find("p#instructions") + sel := doc.Find("p#validEntropyNumber") + if sel.Index() != -1 { + if instructions != sel.Text() { + instructions = sel.Text() + log.Println("Open your Microsoft Authenticator app and tap the number you see below to sign in.") + log.Println(instructions) + } + } + sel = doc.Find("p#instructions") if sel.Index() != -1 { if instructions != sel.Text() { instructions = sel.Text() diff --git a/pkg/provider/adfs2/adfs2_test.go b/pkg/provider/adfs2/adfs2_test.go new file mode 100644 index 000000000..c650926e6 --- /dev/null +++ b/pkg/provider/adfs2/adfs2_test.go @@ -0,0 +1,69 @@ +package adfs2 + +import ( + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/versent/saml2aws/v2/mocks" + "github.com/versent/saml2aws/v2/pkg/cfg" + "github.com/versent/saml2aws/v2/pkg/creds" + "github.com/versent/saml2aws/v2/pkg/prompter" +) + +func TestADFS2RSA(t *testing.T) { + svr := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authresp1 := fmt.Sprintf(`
+ + + +
`, r.Host) + passcoderesp1 := fmt.Sprintf(`
+ + + +
`, r.Host) + rsaresp1 := fmt.Sprintf(`
+ + +
`, r.Host) + if strings.HasPrefix(r.URL.String(), "/adfs/ls/IdpInitiatedSignOn.aspx") { + _, err := w.Write([]byte(authresp1)) + assert.Nil(t, err) + } else if strings.HasPrefix(r.URL.String(), "/authpost1") { + _, err := w.Write([]byte(passcoderesp1)) + assert.Nil(t, err) + } else if strings.HasPrefix(r.URL.String(), "/passcodepost1") { + _, err := w.Write([]byte(rsaresp1)) + assert.Nil(t, err) + } else { + t.Fatalf("unexpected %v", r) + } + })) + defer svr.Close() + idpAccount := cfg.NewIDPAccount() + idpAccount.URL = svr.URL + idpAccount.MFA = "RSA" + idpAccount.Username = "user@example.com" + idpAccount.SkipVerify = true + + loginDetails := &creds.LoginDetails{ + Username: idpAccount.Username, + Password: "abc123", + URL: idpAccount.URL, + } + + pr := &mocks.Prompter{} + prompter.SetPrompter(pr) + pr.Mock.On("Password", "Enter nextCode").Return("5309") + pr.Mock.On("Password", "Enter passcode").Return("0953") + + ac, err := New(idpAccount) + assert.Nil(t, err) + resp, err := ac.Authenticate(loginDetails) + assert.Nil(t, err) + assert.Equal(t, resp, "saml1") +} diff --git a/pkg/provider/adfs2/ntlm.go b/pkg/provider/adfs2/ntlm.go index cd51ad2d3..f78696e8a 100644 --- a/pkg/provider/adfs2/ntlm.go +++ b/pkg/provider/adfs2/ntlm.go @@ -3,7 +3,7 @@ package adfs2 import ( "bytes" "fmt" - "io/ioutil" + "io" "net/http" "github.com/PuerkitoBio/goquery" @@ -30,7 +30,7 @@ func (ac *Client) authenticateNTLM(loginDetails *creds.LoginDetails) (string, er return "", errors.Wrap(err, "error retieving login form") } - data, err := ioutil.ReadAll(res.Body) + data, err := io.ReadAll(res.Body) if err != nil { return "", errors.Wrap(err, "error retieving body") } diff --git a/pkg/provider/adfs2/rsa.go b/pkg/provider/adfs2/rsa.go index 5dca411cc..dfd11efc8 100644 --- a/pkg/provider/adfs2/rsa.go +++ b/pkg/provider/adfs2/rsa.go @@ -3,7 +3,7 @@ package adfs2 import ( "bytes" "fmt" - "io/ioutil" + "io" "net/http" "net/url" "strings" @@ -30,7 +30,7 @@ func (ac *Client) authenticateRsa(loginDetails *creds.LoginDetails) (string, err passcodeForm, passcodeActionURL, err := extractFormData(doc) if err != nil { - return "", errors.Wrap(err, "error extractign login data") + return "", errors.Wrap(err, "error extracting login data") } /** @@ -180,7 +180,7 @@ func (ac *Client) postRSAForm(rsaSubmitURL string, form url.Values) (*goquery.Do logger.WithField("status", res.StatusCode).WithField("rsaSubmitURL", rsaSubmitURL).WithField("res", dump.ResponseString(res)).Debug("POST") - data, err := ioutil.ReadAll(res.Body) + data, err := io.ReadAll(res.Body) if err != nil { return nil, errors.Wrap(err, "error retrieving body") } diff --git a/pkg/provider/adfs2/rsa_test.go b/pkg/provider/adfs2/rsa_test.go index 9d5c60f50..b9639507a 100644 --- a/pkg/provider/adfs2/rsa_test.go +++ b/pkg/provider/adfs2/rsa_test.go @@ -1,7 +1,6 @@ package adfs2 import ( - "io/ioutil" "net/http" "net/http/httptest" "net/url" @@ -17,7 +16,7 @@ import ( func TestClient_getLoginForm(t *testing.T) { - data, err := ioutil.ReadFile("example/loginpage.html") + data, err := os.ReadFile("example/loginpage.html") require.Nil(t, err) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -44,7 +43,7 @@ func TestClient_getLoginForm(t *testing.T) { func TestClient_postLoginForm(t *testing.T) { - data, err := ioutil.ReadFile("example/passcode.html") + data, err := os.ReadFile("example/passcode.html") require.Nil(t, err) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/provider/akamai/akamai.go b/pkg/provider/akamai/akamai.go index c3fd83401..6b42db627 100644 --- a/pkg/provider/akamai/akamai.go +++ b/pkg/provider/akamai/akamai.go @@ -4,7 +4,7 @@ import ( "bytes" "fmt" "html" - "io/ioutil" + "io" "log" "net/http" "net/url" @@ -159,7 +159,7 @@ func (oc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) return samlAssertion, errors.Wrap(err, "error login to EAA IDP") } - body, err := ioutil.ReadAll(res.Body) + body, err := io.ReadAll(res.Body) if err != nil { return samlAssertion, errors.Wrap(err, "error retrieving body from response") } @@ -194,7 +194,7 @@ func (oc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) return samlAssertion, errors.Wrap(err, "error while navigation request to EAA ") } - body, err = ioutil.ReadAll(res.Body) + body, err = io.ReadAll(res.Body) if err != nil { return samlAssertion, errors.Wrap(err, "error retrieving response from navigate request") } @@ -232,7 +232,7 @@ func (oc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) return samlAssertion, errors.Wrap(err, "error navigate request to EAA ") } - body, err = ioutil.ReadAll(res.Body) + body, err = io.ReadAll(res.Body) if err != nil { return samlAssertion, errors.Wrap(err, "error retrieving body from response") } @@ -267,7 +267,7 @@ func verifyMfa(oc *Client, akamaiOrgHost string, loginDetails *creds.LoginDetail if err != nil { return errors.Wrap(err, "error mfa config request to EAA ") } - body, err := ioutil.ReadAll(res.Body) + body, err := io.ReadAll(res.Body) if err != nil { return errors.Wrap(err, "error retrieving mfa config request") } @@ -300,7 +300,7 @@ func verifyMfa(oc *Client, akamaiOrgHost string, loginDetails *creds.LoginDetail if err != nil { return errors.Wrap(err, "error mfa setting request to EAA ") } - mfaSettingData, err := ioutil.ReadAll(res.Body) + mfaSettingData, err := io.ReadAll(res.Body) if err != nil { return errors.Wrap(err, "error retrieving body from response") } @@ -376,7 +376,7 @@ func verifyMfa(oc *Client, akamaiOrgHost string, loginDetails *creds.LoginDetail return errors.Wrap(err, "error while sending MFA push code ") } - body, err = ioutil.ReadAll(res.Body) + body, err = io.ReadAll(res.Body) if err != nil { return errors.Wrap(err, "error retrieving MFA push response ") } @@ -413,7 +413,7 @@ func verifyMfa(oc *Client, akamaiOrgHost string, loginDetails *creds.LoginDetail return errors.Wrap(err, "error verifying mfa to EAA ") } - body, err = ioutil.ReadAll(res.Body) + body, err = io.ReadAll(res.Body) if err != nil { return errors.Wrap(err, "error retrieving mfa verify response") } @@ -523,7 +523,7 @@ func verifyMfa(oc *Client, akamaiOrgHost string, loginDetails *creds.LoginDetail return errors.Wrap(err, "error retrieving duo prompt request") } - body, err = ioutil.ReadAll(res.Body) + body, err = io.ReadAll(res.Body) if err != nil { return errors.Wrap(err, "error retrieving duo prompt response") } @@ -555,7 +555,7 @@ func verifyMfa(oc *Client, akamaiOrgHost string, loginDetails *creds.LoginDetail return errors.Wrap(err, "error sending duo status request") } - body, err = ioutil.ReadAll(res.Body) + body, err = io.ReadAll(res.Body) if err != nil { return errors.Wrap(err, "error retrieving duo status response") } @@ -584,7 +584,7 @@ func verifyMfa(oc *Client, akamaiOrgHost string, loginDetails *creds.LoginDetail return errors.Wrap(err, "error retrieving verify response") } - body, err = ioutil.ReadAll(res.Body) + body, err = io.ReadAll(res.Body) if err != nil { return errors.Wrap(err, "error retrieving body from response") } @@ -619,7 +619,7 @@ func verifyMfa(oc *Client, akamaiOrgHost string, loginDetails *creds.LoginDetail return errors.Wrap(err, "error retrieving duo result response") } - body, err = ioutil.ReadAll(res.Body) + body, err = io.ReadAll(res.Body) if err != nil { return errors.Wrap(err, "duoResultSubmit: error retrieving body from response") } @@ -655,7 +655,7 @@ func verifyMfa(oc *Client, akamaiOrgHost string, loginDetails *creds.LoginDetail return errors.Wrap(err, "error sending duo mfa request to EAA ") } - body, err = ioutil.ReadAll(res.Body) + body, err = io.ReadAll(res.Body) if err != nil { return errors.Wrap(err, "error retrieving duo mfa response ") } diff --git a/pkg/provider/auth0/auth0.go b/pkg/provider/auth0/auth0.go index d67266c7e..4683214b2 100644 --- a/pkg/provider/auth0/auth0.go +++ b/pkg/provider/auth0/auth0.go @@ -6,7 +6,7 @@ import ( "encoding/json" "fmt" "html" - "io/ioutil" + "io" "net/http" "net/url" "regexp" @@ -86,7 +86,7 @@ type sessionInfo struct { csrf string } -//authCallbackRequest represents Auth0 authentication callback request +// authCallbackRequest represents Auth0 authentication callback request type authCallbackRequest struct { method string url string @@ -207,7 +207,7 @@ func (ac *Client) fetchSessionInfo(loginURL string) (*sessionInfo, error) { return nil, errors.Wrap(err, "error retrieving response") } - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, errors.Wrap(err, "error retrieving response body") } @@ -249,7 +249,7 @@ func (ac *Client) getConnectionNames(connectionInfoURL string) ([]string, error) return nil, errors.Wrap(err, "error retrieving response") } - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, errors.Wrap(err, "error retrieving body from response") } @@ -340,7 +340,7 @@ func (ac *Client) loginAuth0(loginDetails *creds.LoginDetails, ai *authInfo) (st return "", errors.Wrap(err, "error retrieving auth response") } - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { return "", errors.Wrap(err, "error retrieving body from response") } @@ -364,7 +364,7 @@ func (ac *Client) doAuthCallback(authCallback *authCallbackRequest, ai *authInfo return "", errors.Wrap(err, "error retrieving auth callback response") } - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { return "", errors.Wrap(err, "error retrieving body from response") } diff --git a/pkg/provider/authentik/authentik.go b/pkg/provider/authentik/authentik.go new file mode 100644 index 000000000..e7cb49d5c --- /dev/null +++ b/pkg/provider/authentik/authentik.go @@ -0,0 +1,283 @@ +package authentik + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/versent/saml2aws/v2/pkg/cfg" + "github.com/versent/saml2aws/v2/pkg/creds" + "github.com/versent/saml2aws/v2/pkg/provider" +) + +// Client wrapper around authentik. +type Client struct { + provider.ValidateBase + + client *provider.HTTPClient +} + +var logger = logrus.WithField("provider", "authentik") + +// New create a new client +func New(idpAccount *cfg.IDPAccount) (*Client, error) { + tr := provider.NewDefaultTransport(idpAccount.SkipVerify) + + client, err := provider.NewHTTPClient(tr, provider.BuildHttpClientOpts(idpAccount)) + if err != nil { + return nil, errors.Wrap(err, "error building http client") + } + + client.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + return &Client{ + client: client, + }, nil +} + +// Authenticate Log into authentik and returns a SAML response +func (kc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) { + ctx := &authentikContext{ + loginDetails: loginDetails, + } + samlResponse, err := kc.auth(ctx) + if err != nil { + return "", errors.Wrap(err, "error retrieving saml response from idp") + } + + return samlResponse, err +} + +// auth Authentication +func (kc *Client) auth(ctx *authentikContext) (string, error) { + logger.Debug("[GET] ", ctx.loginDetails.URL) + res, err := kc.client.Get(ctx.loginDetails.URL) + if err != nil { + return "", errors.Wrap(err, "error retrieving initial url") + } + if res.StatusCode == http.StatusFound { + var location *url.URL + location, err = res.Location() + if err != nil { + return "", err + } + err = ctx.updateURL(location.String()) + if err != nil { + return "", err + } + + return kc.auth(ctx) + } + + requestURL := res.Request.URL + if len(res.Cookies()) > 0 { + baseURL := &url.URL{Scheme: requestURL.Scheme, Host: requestURL.Host, Path: "/"} + kc.client.Jar.SetCookies(baseURL, res.Cookies()) + } + + next, err := kc.processQuery(ctx) + if err != nil { + return "", err + } + if ctx.samlResponse != "" { + return ctx.samlResponse, nil + } + + err = ctx.updateURL(next) + if err != nil { + return "", err + } + return kc.auth(ctx) +} + +// processQuery Loop to get the authentik credentials +func (kc *Client) processQuery(ctx *authentikContext) (string, error) { + var shouldContinue bool + var next string + var err error + + next, err = queryNextURL(ctx.loginDetails.URL) + if err != nil { + return "", err + } + err = ctx.updateURL(next) + if err != nil { + return "", err + } + + for { + shouldContinue, next, err = kc.queryNext(ctx) + if err != nil { + return "", err + } + if next != "" { + err = ctx.updateURL(next) + if err != nil { + return "", err + } + } + if !shouldContinue { + break + } + } + + return next, nil +} + +// queryNext Do query and submit infos +func (kc *Client) queryNext(ctx *authentikContext) (bool, string, error) { + logger.Debug("[GET] ", ctx.loginDetails.URL) + res, err := kc.client.Get(ctx.loginDetails.URL) + if err != nil { + return false, "", err + } + if res.StatusCode == http.StatusFound { + next, err1 := res.Location() + if err1 != nil { + return false, "", err1 + } + err = ctx.updateURL(next.String()) + if err != nil { + return false, "", err + } + + return kc.queryNext(ctx) + } + var payload *authentikPayload + payload, err = parseResponsePayload(res) + if err != nil { + return false, "", err + } + if payload.isTypeRedirect() { + // login success if there is a redirect + logger.Debug("Login success, redirect to saml response") + return false, payload.RedirectTo, nil + } else if !payload.isTypeNative() { + return false, "", errors.New("Unknown type: " + payload.Type) + } + + if payload.isComponentStageAutosubmit() { + ctx.setSAMLResponse(payload.Attrs["SAMLResponse"]) + return false, "", nil + } + + next, err := kc.doPostQuery(ctx, payload) + return true, next, err +} + +// doPostQuery For all data setting operations +func (kc *Client) doPostQuery(ctx *authentikContext, payload *authentikPayload) (string, error) { + data, err := getLoginJSON(ctx.loginDetails, payload) + if err != nil { + return "", err + } + + logger.Debug("[POST]", ctx.loginDetails.URL) + res, err := kc.client.Post(ctx.loginDetails.URL, "application/json", bytes.NewReader(data)) + if err != nil { + return "", err + } + if res.StatusCode == http.StatusOK { + var payload *authentikPayload + payload, err = parseResponsePayload(res) + if err != nil { + return "", err + } + + var errMsg string + if len(payload.Errors) > 0 { + errMsg = prepareErrors(payload.Component, payload.Errors) + } else { + errMsg = "Unexpected" + } + + return "", errors.New(errMsg) + } + loc, err := res.Location() + return loc.String(), err +} + +// getLoginJSON Generate the login json +func getLoginJSON(loginDetails *creds.LoginDetails, payload *authentikPayload) ([]byte, error) { + component := payload.Component + m := map[string]string{ + "component": component, + } + switch component { + case "ak-stage-identification": + m["uid_field"] = loginDetails.Username + if payload.HasPassowrdField { + m["password"] = loginDetails.Password + } + case "ak-stage-password": + m["password"] = loginDetails.Password + default: + return []byte(""), errors.New("unknown component: " + component) + } + + return json.Marshal(m) +} + +// queryNextURL Get the next api url +func queryNextURL(u string) (string, error) { + next, err := url.Parse(u) + if err != nil { + return "", errors.New("Invalid url") + } + + result := strings.Split(next.Path, "/") + flow := result[len(result)-2] + return fmt.Sprintf("%s://%s/api/v3/flows/executor/%s/?query=%s", next.Scheme, next.Host, flow, url.QueryEscape(next.RawQuery)), nil +} + +// getFieldName Get name of component +func getFieldName(component string) (string, error) { + prefix := "ak-stage-" + if strings.Index(component, prefix) != 0 { + return "", errors.New("") + } + s := strings.Split(component, "ak-stage-") + return s[len(s)-1], nil +} + +// prepareErrors Transform errors to string +func prepareErrors(component string, errs map[string][]map[string]string) string { + field, err := getFieldName(component) + if err != nil { + return "Invalid component" + } + + key := "non_field_errors" + if field == "password" { + key = "password" + } + msgs := make([]string, 0, len(errs[key])) + for _, err := range errs[key] { + msgs = append(msgs, fmt.Sprintf("%s %s: %s", field, err["code"], err["string"])) + } + return strings.Join(msgs, "; ") +} + +// parseResponsePayload Parse response from authentik api +func parseResponsePayload(res *http.Response) (*authentikPayload, error) { + var payload authentikPayload + defer res.Body.Close() + b, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + err = json.Unmarshal(b, &payload) + if err != nil { + return nil, err + } + + return &payload, nil +} diff --git a/pkg/provider/authentik/authentik_test.go b/pkg/provider/authentik/authentik_test.go new file mode 100644 index 000000000..78c15c249 --- /dev/null +++ b/pkg/provider/authentik/authentik_test.go @@ -0,0 +1,320 @@ +package authentik + +import ( + "testing" + + "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + + "github.com/versent/saml2aws/v2/pkg/cfg" + "github.com/versent/saml2aws/v2/pkg/creds" +) + +func Test_getLoginJSON(t *testing.T) { + assert := assert.New(t) + loginDetails := &creds.LoginDetails{ + Username: "user", + Password: "pwd", + URL: "https://127.0.0.1/sso/init", + } + payload := &authentikPayload{ + Component: "ak-stage-identification", + Type: "native", + } + b, err := getLoginJSON(loginDetails, payload) + assert.Nil(err) + assert.Equal(string(b), "{\"component\":\"ak-stage-identification\",\"uid_field\":\"user\"}") + + payload = &authentikPayload{ + Component: "ak-stage-password", + Type: "native", + } + b, err = getLoginJSON(loginDetails, payload) + assert.Nil(err) + assert.Equal(string(b), "{\"component\":\"ak-stage-password\",\"password\":\"pwd\"}") + + payload = &authentikPayload{ + Component: "ak-stage-test", + Type: "native", + } + _, err = getLoginJSON(loginDetails, payload) + assert.NotNil(err) +} + +func Test_queryNextURL(t *testing.T) { + assert := assert.New(t) + url, err := queryNextURL("https://127.0.0.1/if/flow/default-authentication-flow/?next=/application/saml/aws/sso/binding/init/") + assert.Nil(err) + assert.Equal(url, "https://127.0.0.1/api/v3/flows/executor/default-authentication-flow/?query=next%3D%2Fapplication%2Fsaml%2Faws%2Fsso%2Fbinding%2Finit%2F") +} + +func Test_getFieldName(t *testing.T) { + assert := assert.New(t) + var name string + var err error + name, err = getFieldName("ak-stage-identification") + assert.Nil(err) + assert.Equal(name, "identification") + + name, err = getFieldName("ak-stage-password") + assert.Nil(err) + assert.Equal(name, "password") + + name, err = getFieldName("ak-stage-") + assert.Nil(err) + assert.Equal(name, "") + + name, err = getFieldName("stage-password") + assert.NotNil(err) + assert.Equal(name, "") +} + +func Test_prepareErrors(t *testing.T) { + assert := assert.New(t) + var desc string + identification_errs := map[string][]map[string]string{ + "non_field_errors": { + { + "string": "Failed to authenticate.", + "code": "invalid", + }, + }, + } + desc = prepareErrors("ak-stage-identification", identification_errs) + assert.Equal(desc, "identification invalid: Failed to authenticate.") + + desc = prepareErrors("ak-stage-password", identification_errs) + assert.Equal(desc, "") + + passwordErrs := map[string][]map[string]string{ + "password": { + { + "string": "Failed to authenticate.", + "code": "invalid", + }, + }, + } + desc = prepareErrors("ak-stage-password", passwordErrs) + assert.Equal(desc, "password invalid: Failed to authenticate.") + + desc = prepareErrors("ak-stage-identification", passwordErrs) + assert.Equal(desc, "") +} + +// Test_authWithCombinedUsernamePassword Password only if username/email verified +func Test_authWithSeperatedUsernamePassword(t *testing.T) { + defer gock.Off() + samlResponse := "PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaX" + gock.New("http://127.0.0.1"). + Get("/application/saml/aws/sso/binding/init"). + Reply(302). + SetHeader("Set-Cookie", "[authentik_session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzaWQiOiJ6cHI3NGdzMjNnOGNqbmF1bXNheGQ1dXVrc2VtZGZpNyIsImlzcyI6ImF1dGhlbnRpayIsInN1YiI6ImFub255bW91cyIsImF1dGhlbnRpY2F0ZWQiOmZhbHNlLCJhY3IiOiJnb2F1dGhlbnRpay5pby9jb3JlL2RlZmF1bHQifQ.zNiX4pk6G9ABeDip0PLs8-0irm2aQ_Arr_RgTxTGCQM; HttpOnly; Path=/; SameSite=None; Secure]"). + SetHeader("Location", "/flows/-/default/authentication/?next=/application/saml/aws/sso/binding/init/") + + gock.New("http://127.0.0.1"). + Get("/flows/-/default/authentication"). + Reply(302). + SetHeader("Location", "/if/flow/default-authentication-flow/?next=%2Fapplication%2Fsaml%2Faws%2Fsso%2Fbinding%2Finit%2F") + + gock.New("http://127.0.0.1"). + Get("/if/flow/default-authentication-flow"). + Reply(200). + BodyString("") + + gock.New("http://127.0.0.1"). + Get("/api/v3/flows/executor/default-authentication-flow"). + Reply(200). + JSON(map[string]interface{}{ + "type": "native", + "flow_info": map[string]interface{}{"title": "Welcome to authentik!", "background": "/static/dist/assets/images/flow_background.jpg", "cancel_url": "/flows/-/cancel/", "layout": "stacked"}, + "component": "ak-stage-identification", + "user_fields": []string{"username", "email"}, + "password_fields": false, + "application_pre": "aws", + "primary_action": "Log in", + "sources": []string{}, + "show_source_labels": false, + }) + + gock.New("http://127.0.0.1"). + Post("/api/v3/flows/executor/default-authentication-flow"). + Reply(302). + SetHeader("Location", "/api/v3/flows/executor/default-authentication-flow/?query=next%3D%252F") + + gock.New("http://127.0.0.1"). + Get("api/v3/flows/executor/default-authentication-flow"). + Reply(200). + JSON(map[string]interface{}{ + "type": "native", + "flow_info": map[string]interface{}{"title": "Welcome to authentik!", "background": "/static/dist/assets/images/flow_background.jpg", "cancel_url": "/flows/-/cancel/", "layout": "stacked"}, + "component": "ak-stage-password", + "pending_user": "user", + "pending_user_avatar": "https://secure.gravatar.com/avatar/0932141298741243?s=158&r=g", + }) + gock.New("http://127.0.0.1"). + Post("/api/v3/flows/executor/default-authentication-flow"). + Reply(302). + SetHeader("Location", "/api/v3/flows/executor/default-authentication-flow/?query=next%3D%252F") + + gock.New("http://127.0.0.1"). + Get("/api/v3/flows/executor/default-authentication-flow"). + Reply(302). + SetHeader("Location", "/api/v3/flows/executor/default-authentication-flow/?query=next%3D%252F") + + gock.New("http://127.0.0.1"). + Get("/api/v3/flows/executor/default-authentication-flow"). + Reply(200). + JSON(map[string]interface{}{ + "type": "redirect", + "to": "http://127.0.0.1/application/saml/aws/sso/binding/init", + }) + + gock.New("http://127.0.0.1"). + Get("/application/saml/aws/sso/binding/init"). + Reply(302). + SetHeader("Location", "/if/flow/default-provider-authorization-implicit-consent/") + + gock.New("http://127.0.0.1"). + Get("/if/flow/default-provider-authorization-implicit-consent/"). + Reply(200) + + gock.New("http://127.0.0.1"). + Get("/api/v3/flows"). + Reply(200). + JSON(map[string]interface{}{ + "type": "native", + "flow_info": map[string]interface{}{ + "title": "Redirecting to aws", + "background": "/static/dist/assets/images/flow_background.jpg", + "cancel_url": "/flows/-/cancel/", + "layout": "stacked", + }, + "component": "ak-stage-autosubmit", + "url": "https://signin.amazonaws.com/saml", + "attrs": map[string]interface{}{ + "ACSUrl": "https://signin.amazonaws.com/saml", + "SAMLResponse": samlResponse, + }, + }) + client, _ := New(&cfg.IDPAccount{}) + loginDetails := &creds.LoginDetails{ + Username: "user", + Password: "pwd", + URL: "http://127.0.0.1/application/saml/aws/sso/binding/init", + } + gock.InterceptClient(&client.client.Client) + result, err := client.Authenticate(loginDetails) + + assert := assert.New(t) + assert.Nil(err) + assert.Equal(result, samlResponse) +} + +// Test_authWithCombinedUsernamePassword Username/email and password in one page +func Test_authWithCombinedUsernamePassword(t *testing.T) { + defer gock.Off() + samlResponse := "PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaX" + gock.New("http://127.0.0.1"). + Get("/application/saml/aws/sso/binding/init"). + Reply(302). + SetHeader("Set-Cookie", "[authentik_session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzaWQiOiJ6cHI3NGdzMjNnOGNqbmF1bXNheGQ1dXVrc2VtZGZpNyIsImlzcyI6ImF1dGhlbnRpayIsInN1YiI6ImFub255bW91cyIsImF1dGhlbnRpY2F0ZWQiOmZhbHNlLCJhY3IiOiJnb2F1dGhlbnRpay5pby9jb3JlL2RlZmF1bHQifQ.zNiX4pk6G9ABeDip0PLs8-0irm2aQ_Arr_RgTxTGCQM; HttpOnly; Path=/; SameSite=None; Secure]"). + SetHeader("Location", "/flows/-/default/authentication/?next=/application/saml/aws/sso/binding/init/") + + gock.New("http://127.0.0.1"). + Get("/flows/-/default/authentication"). + Reply(302). + SetHeader("Location", "/if/flow/default-authentication-flow/?next=%2Fapplication%2Fsaml%2Faws%2Fsso%2Fbinding%2Finit%2F") + + gock.New("http://127.0.0.1"). + Get("/if/flow/default-authentication-flow"). + Reply(200). + BodyString("") + + gock.New("http://127.0.0.1"). + Get("/api/v3/flows/executor/default-authentication-flow"). + Reply(200). + JSON(map[string]interface{}{ + "type": "native", + "flow_info": map[string]interface{}{"title": "Welcome to authentik!", "background": "/static/dist/assets/images/flow_background.jpg", "cancel_url": "/flows/-/cancel/", "layout": "stacked"}, + "component": "ak-stage-identification", + "user_fields": []string{"username", "email"}, + "password_fields": true, + "application_pre": "aws", + "primary_action": "Log in", + "sources": []string{}, + "show_source_labels": false, + }) + + gock.New("http://127.0.0.1"). + Post("/api/v3/flows/executor/default-authentication-flow"). + Reply(302). + SetHeader("Location", "/api/v3/flows/executor/default-authentication-flow/?query=next%3D%252F") + + gock.New("http://127.0.0.1"). + Get("api/v3/flows/executor/default-authentication-flow"). + Reply(200). + JSON(map[string]interface{}{ + "type": "native", + "flow_info": map[string]interface{}{"title": "Welcome to authentik!", "background": "/static/dist/assets/images/flow_background.jpg", "cancel_url": "/flows/-/cancel/", "layout": "stacked"}, + "component": "ak-stage-password", + "pending_user": "user", + "pending_user_avatar": "https://secure.gravatar.com/avatar/0932141298741243?s=158&r=g", + }) + gock.New("http://127.0.0.1"). + Post("/api/v3/flows/executor/default-authentication-flow"). + Reply(302). + SetHeader("Location", "/api/v3/flows/executor/default-authentication-flow/?query=next%3D%252F") + + gock.New("http://127.0.0.1"). + Get("/api/v3/flows/executor/default-authentication-flow"). + Reply(302). + SetHeader("Location", "/api/v3/flows/executor/default-authentication-flow/?query=next%3D%252F") + + gock.New("http://127.0.0.1"). + Get("/api/v3/flows/executor/default-authentication-flow"). + Reply(200). + JSON(map[string]interface{}{ + "type": "redirect", + "to": "http://127.0.0.1/application/saml/aws/sso/binding/init", + }) + + gock.New("http://127.0.0.1"). + Get("/application/saml/aws/sso/binding/init"). + Reply(302). + SetHeader("Location", "/if/flow/default-provider-authorization-implicit-consent/") + + gock.New("http://127.0.0.1"). + Get("/if/flow/default-provider-authorization-implicit-consent/"). + Reply(200) + + gock.New("http://127.0.0.1"). + Get("/api/v3/flows"). + Reply(200). + JSON(map[string]interface{}{ + "type": "native", + "flow_info": map[string]interface{}{ + "title": "Redirecting to aws", + "background": "/static/dist/assets/images/flow_background.jpg", + "cancel_url": "/flows/-/cancel/", + "layout": "stacked", + }, + "component": "ak-stage-autosubmit", + "url": "https://signin.amazonaws.com/saml", + "attrs": map[string]interface{}{ + "ACSUrl": "https://signin.amazonaws.com/saml", + "SAMLResponse": samlResponse, + }, + }) + client, _ := New(&cfg.IDPAccount{}) + loginDetails := &creds.LoginDetails{ + Username: "user", + Password: "pwd", + URL: "http://127.0.0.1/application/saml/aws/sso/binding/init", + } + gock.InterceptClient(&client.client.Client) + result, err := client.Authenticate(loginDetails) + + assert := assert.New(t) + assert.Nil(err) + assert.Equal(result, samlResponse) +} diff --git a/pkg/provider/authentik/model.go b/pkg/provider/authentik/model.go new file mode 100644 index 000000000..1bffe7d9f --- /dev/null +++ b/pkg/provider/authentik/model.go @@ -0,0 +1,54 @@ +package authentik + +import ( + "fmt" + "net/url" + "strings" + + "github.com/pkg/errors" + + "github.com/versent/saml2aws/v2/pkg/creds" +) + +type authentikContext struct { + loginDetails *creds.LoginDetails + samlResponse string +} + +type authentikPayload struct { + Attrs map[string]string + Component string + Type string + HasPassowrdField bool `json:"password_fields"` + RedirectTo string `json:"to"` + Errors map[string][]map[string]string `json:"response_errors"` +} + +func (ctx *authentikContext) updateURL(s string) error { + if strings.Index(s, "/") == 0 { + u, err := url.Parse(ctx.loginDetails.URL) + if err != nil { + return errors.New("Invalid url") + } + s = fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, s) + } + + ctx.loginDetails.URL = s + return nil +} + +func (ctx *authentikContext) setSAMLResponse(val string) { + ctx.samlResponse = val +} + +func (payload *authentikPayload) isTypeNative() bool { + return payload.Type == "native" +} + +func (payload *authentikPayload) isTypeRedirect() bool { + return payload.Type == "redirect" +} + +func (payload *authentikPayload) isComponentStageAutosubmit() bool { + return payload.Component == "ak-stage-autosubmit" +} diff --git a/pkg/provider/authentik/model_test.go b/pkg/provider/authentik/model_test.go new file mode 100644 index 000000000..62df451fd --- /dev/null +++ b/pkg/provider/authentik/model_test.go @@ -0,0 +1,26 @@ +package authentik + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/versent/saml2aws/v2/pkg/creds" +) + +func Test_updateURL(t *testing.T) { + assert := assert.New(t) + ctx := &authentikContext{ + loginDetails: &creds.LoginDetails{ + Username: "user", + Password: "pwd", + URL: "https://127.0.0.1/sso/init", + }, + } + err := ctx.updateURL("/query?next=/login") + assert.Nil(err) + assert.Equal(ctx.loginDetails.URL, "https://127.0.0.1/query?next=/login") + + err = ctx.updateURL("https://127.0.0.1:8888/sso/aws") + assert.Nil(err) + assert.Equal(ctx.loginDetails.URL, "https://127.0.0.1:8888/sso/aws") +} diff --git a/pkg/provider/browser/browser.go b/pkg/provider/browser/browser.go index d2947bfee..4bea17373 100644 --- a/pkg/provider/browser/browser.go +++ b/pkg/provider/browser/browser.go @@ -2,9 +2,11 @@ package browser import ( "errors" + "fmt" "net/url" + "regexp" - "github.com/mxschmitt/playwright-go" + "github.com/playwright-community/playwright-go" "github.com/sirupsen/logrus" "github.com/versent/saml2aws/v2/pkg/cfg" "github.com/versent/saml2aws/v2/pkg/creds" @@ -14,23 +16,41 @@ var logger = logrus.WithField("provider", "browser") // Client client for browser based Identity Provider type Client struct { + Headless bool + // Setup alternative directory to download playwright browsers to + BrowserDriverDir string } // New create new browser based client func New(idpAccount *cfg.IDPAccount) (*Client, error) { - return &Client{}, nil + return &Client{ + Headless: idpAccount.Headless, + BrowserDriverDir: idpAccount.BrowserDriverDir, + }, nil } func (cl *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) { + runOptions := playwright.RunOptions{} + if cl.BrowserDriverDir != "" { + runOptions.DriverDirectory = cl.BrowserDriverDir + } + + // Optionally download browser drivers if specified + if loginDetails.DownloadBrowser { + err := playwright.Install(&runOptions) + if err != nil { + return "", err + } + } - pw, err := playwright.Run() + pw, err := playwright.Run(&runOptions) if err != nil { return "", err } // TODO: provide some overrides for this window launchOptions := playwright.BrowserTypeLaunchOptions{ - Headless: playwright.Bool(false), + Headless: playwright.Bool(cl.Headless), } // currently using Chromium as it is widely supported for Identity providers @@ -46,35 +66,56 @@ func (cl *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) return "", err } + defer func() { + logger.Info("clean up browser") + if err := browser.Close(); err != nil { + logger.Info("Error when closing browser", err) + } + if err := pw.Stop(); err != nil { + logger.Info("Error when stopping pm", err) + } + }() + + return getSAMLResponse(page, loginDetails) +} + +var getSAMLResponse = func(page playwright.Page, loginDetails *creds.LoginDetails) (string, error) { logger.WithField("URL", loginDetails.URL).Info("opening browser") if _, err := page.Goto(loginDetails.URL); err != nil { return "", err } - r := page.WaitForRequest("https://signin.aws.amazon.com/saml") - data, err := r.PostData() + // https://docs.aws.amazon.com/general/latest/gr/signin-service.html + // https://docs.amazonaws.cn/en_us/aws/latest/userguide/endpoints-Ningxia.html + // https://docs.amazonaws.cn/en_us/aws/latest/userguide/endpoints-Beijing.html + signin_re, err := signinRegex() if err != nil { return "", err } - values, err := url.ParseQuery(data) + fmt.Println("waiting ...") + r := page.WaitForRequest(signin_re) + data, err := r.PostData() if err != nil { return "", err } - logger.Info("clean up browser") - - if err = browser.Close(); err != nil { - return "", err - } - if err = pw.Stop(); err != nil { + values, err := url.ParseQuery(data) + if err != nil { return "", err } return values.Get("SAMLResponse"), nil } +func signinRegex() (*regexp.Regexp, error) { + // https://docs.aws.amazon.com/general/latest/gr/signin-service.html + // https://docs.amazonaws.cn/en_us/aws/latest/userguide/endpoints-Ningxia.html + // https://docs.amazonaws.cn/en_us/aws/latest/userguide/endpoints-Beijing.html + return regexp.Compile(`https:\/\/((.*\.)?signin\.(aws\.amazon\.com|amazonaws-us-gov\.com|amazonaws\.cn))\/saml`) +} + func (cl *Client) Validate(loginDetails *creds.LoginDetails) error { if loginDetails.URL == "" { diff --git a/pkg/provider/browser/browser_test.go b/pkg/provider/browser/browser_test.go new file mode 100644 index 000000000..c21cde716 --- /dev/null +++ b/pkg/provider/browser/browser_test.go @@ -0,0 +1,118 @@ +package browser + +import ( + "net/url" + "testing" + + "github.com/playwright-community/playwright-go" + "github.com/stretchr/testify/assert" + "github.com/versent/saml2aws/v2/mocks" + "github.com/versent/saml2aws/v2/pkg/cfg" + "github.com/versent/saml2aws/v2/pkg/creds" +) + +var response = ` + + http://idp.example.com/metadata.php + + + + + hKoNWraWb2YHo6B8ZFI5wOj8C0zdeYnQpaIE14vqe67LCy9e4Y+q7lMTRa7gNa6WZMJbkj1aQ/omsQLRVMkknKFoGD244J0Or9Ma8aXoEkQuRFyw90G4SkH5KuE6ZjUMJkMZN6xDYC+CozYiD6Pfchth/Ks64dNLJ2REau1dV/0= + + wbjgmIwWtVqv/doV7LTdN7pKkQTQWxpZz1jme5KeB0PidGQLJrHsvNYZpWWixh9b8InsOLmshPJqt5M3CMXqGItxsaQ8rAZim351eYRoDMWPUWydPdZIwoqdhcbrpxmVZsJ0RIjtSPgKBkGFjLL28KTS+JmvMDUdpW8qi+ssr+8661gVuvB5BEp2g+hblb7FGtDJxpYoOumh9zgEqeNiUQ0PcwuRbMq6I0LTWtpA2USlM8Lh+TqfQS/2yL/HioZSby6Uvql+7pOUqwgEzOyeFWepkPtawcasgF6tXIT0b/U6ro9xg4x37+znYjiQCdSMmuiE6ipuBjWc9Zj3sYITVIpOqUTmPoZQ+Jg143/Mjj7Lx4EAnYwJzF6rgrgN20Ep+LCWJ8uPfirK7xz3mq3I5AC1Mv04lASX++vlzFJDXpX2cjeMTQfKQRdzBCEUxn87wP7IeKupOmuBRCIN9kx/cTHkjW8LW2cWQ4xlWKYE0FrvT7k853U0mtz5DlCo3UsQdgp4OKPdwzHDK+/hOjMBwdIeRSZZhLlkVK7v1GesJBrPX5u9dsXjW4jrexeRKntXJ4LBA+bGFjiBKNZci2j85ZC+tQ+TFmBAF/KFyM3U+AU/YzfbGvo/owKL46DIpw8XPeGBBN7mYbp7F7C3RpS67ex4d93FhUo1+c8nFKpfK9crLuJCg3zW/YjVPfhDb7l+i+ckQ2CyIQOffAtrJK1bgq1g7SokgtVbfIXRkhXCB/eGuCDmELuOaPkBpvtHrOy5R2OA/UaB3tkT9q2scWRO94H20Xz5+hUwMnbnnYMXBdhbIGSkisYnL+0CjYyh5NpvpFxczZI4i5N4EFwGGqbBRokVa7fZCyrQRcCnM1UX4F+NGSwebEFoJRcJibCU8pHXEj176TT+ZwJw6h23WZmdFaiZl7x80tLISYPUzkc3phG6ytWzCtY5LokIzi4IUQd8lnqa64tnWljx45BJq7te16Q4TAi9sUy2PeB/Cug3nThTYJOM2VRBhh5BZFZEu/NuCMRoL+/t8GWXgmlHPyXX7hC+EG3P6RaAUmXeOt3bcEY3EbR1lz/+20/y32YBh3nwWWJqwoCwqdymDHN8VqVbJqRkVs5vlVuhJFROR1oh1M3RE7KPtQsxOUkihnieftQqmolq2fNOA/1HRcOmczisgAt02cf7wkQEHtXUpXloTrWUu44eZo++HAt4bPehLE0Y4ojeu174WWjVXEf2o8JGjTj7JVKrKGVwDwHfbbZJhezyYtmApwP70ZJjXxjGmGzW8OoPDS2pBBE+FGyI2drwWqZ5j5EVyHXRqnMcDhS1Sd/WQ53lreedRO+QcvGDUE1mK8j3pYKWO20prGjlYzIEQXJGmxfYVi5IF4hNVNtQ16SiQaHuGwcumkQIxIEbDBFaynj2SBJmwuK/DF/wglKlIassIqgWrFKkk2S04SdUdv7/PFUxbc5u1x/oaluRcDWUnClAPnK52RdxEFr1l0ht6deyEQWokylCNpnBAntmV8muMCdTRYz2qDJC2JBuab3RfCqsXhHzmyVLEHDz7S/xClZVZx01CxqbFTv0x08wWTrDHbtOmBsvAhlfZzTbEyu8SHOb2oMYwilzSHYNndJKV8bctcNUm/54sKzQkkU2xmz8sbrpAofFHEvWNmQ3qRc6XIuU4OJoKgNHnJtwx7/En0MlM/cqVs7yYxKJ3VBtafHJM9h0cs9ZLD1qw7v0nyTPCY13hyTM9N20Bh39SN1gTPg+kZZwhpGRBzi4/LiR96QokFsxCgVOeIeOWuvG/iG53BM6unybZeenKv6OD+w/f2zLhP5ATK9YMQCGh15PHwLUkDJP05dKPwtinU/ywiBdHU3Pr5ZyZN5s/pp5VWh2LI032demuWIWSXirQXc++amCXX1xSKgl4wr/qGBjqg/Qsvo/e2hKwhkWZ+WrkrZ1fvwv27neTH1pVi0FHtNzY9Tp4lJLxmwB126MMmhoQXCF+f/4HOTMa3uJh/htwTOE7eN5yzAEZGX6RyiO8tdlY3B6LBPFEnou9Ha0Nyemw1hhvkdCTydcUGXQ0wEyU+4Sp8YUZAV4x0JFB/0WeNaEDmugCFajknjNDN2QYMMNaATM3jYuVD1zcoYLQKYb9RZbAznXTUGqQFb6RtrSStERCsEKm1/ovf9KiuqYb1ItGOXFQbpcQRXguWHpF5c39ncKmyoIqPIbjCS5DWaNkq+rdUMv5K8KPurY4bpFFli9ytDQD7PFZ6uxeWH9lu6HzS6uzvuSGvx8VQaGyjP5lJZtrFxnj6K4Ev6duvMafJnrzhIUpl6FimmW3JOjTQIKobyW/hhQHxDVf1zDq0m/UEvXoUVVMiFg9QELCd2pNpgGcc2aSeIsc5vMdnMMBcTfLdKs7FAYMFuKh2e5nJdhWUam97HbtOnzsT04B+EsRNbLyqgf+x54yN5/xxtg7N+cUQ8IZcOwk3+kGzmaq681wFQ6PnBNFhUOFKvAhC20EPXyANtTGFr6LvvxPfUmnXkTJE5hLqkgy4qZDgJrARfPOPe1mcwu/m8ttrxcEYso95nBMZblI0UC7bp7QT2xCdGvbi8Zwi0OVbshlVx6PDbDll1f0rEgxAoYUSEF3zrjW/vRk8njBKAt/vmmI0/aDHYZlnVJG4AbVQ+T4UAWCVgJJIuCRN4Owh4m92a8p4cgqB+3PKIWceyS0je4RfOjEpRql+VJrPx58qKJuXXW2aBWHay7QSsaPuseCuP3DKaUKYiLLl/Q7hCIhgImte5l7RKl2rlDE8i0A7/p7zT6rTP3+1jbEIeYyw2T33mq15hGKt/acUjsS++8lfLURcPU1vNpwg75Y0ry+fl1vGwBtwkqZRD8ZoBhL7iyxOwbL8iD97eO9tgvYDYhrJjjpfiuke4ReUu261YabAaS858VxZotuLlTT/g= + + + + +` + +func TestValidate(t *testing.T) { + currentSAMLResponse := getSAMLResponse + defer func() { + getSAMLResponse = currentSAMLResponse + }() + getSAMLResponse = fakeSAMLResponse + account := &cfg.IDPAccount{ + Headless: true, + } + client, err := New(account) + assert.Nil(t, err) + loginDetails := &creds.LoginDetails{ + URL: "https://google.com/", + DownloadBrowser: true, + } + resp, err := client.Authenticate(loginDetails) + assert.Nil(t, err) + assert.Equal(t, resp, response) +} + +// Test that if download directory does not have browsers, it fails with expected error message +func TestNoBrowserDriverFail(t *testing.T) { + account := &cfg.IDPAccount{ + Headless: true, + BrowserDriverDir: t.TempDir(), // set up a directory we know won't have drivers + } + loginDetails := &creds.LoginDetails{ + URL: "https://google.com/", + } + client, _ := New(account) + _, err := client.Authenticate(loginDetails) + assert.Error(t, err) + assert.ErrorContains(t, err, "could not start driver") +} + +func fakeSAMLResponse(page playwright.Page, loginDetails *creds.LoginDetails) (string, error) { + return response, nil +} + +func TestSigninRegex1(t *testing.T) { + regex, err := signinRegex() + assert.Nil(t, err) + match := regex.MatchString("https://signin.aws.amazon.com/saml") + assert.True(t, match) +} + +func TestSigninRegexFail(t *testing.T) { + regex, err := signinRegex() + assert.Nil(t, err) + match := regex.MatchString("https://google.com/") + assert.False(t, match) +} + +func TestGetSAMLResponse(t *testing.T) { + samlp := ` + + http://idp.example.com/metadata.php + + + + + hKoNWraWb2YHo6B8ZFI5wOj8C0zdeYnQpaIE14vqe67LCy9e4Y+q7lMTRa7gNa6WZMJbkj1aQ/omsQLRVMkknKFoGD244J0Or9Ma8aXoEkQuRFyw90G4SkH5KuE6ZjUMJkMZN6xDYC+CozYiD6Pfchth/Ks64dNLJ2REau1dV/0= + + wbjgmIwWtVqv/doV7LTdN7pKkQTQWxpZz1jme5KeB0PidGQLJrHsvNYZpWWixh9b8InsOLmshPJqt5M3CMXqGItxsaQ8rAZim351eYRoDMWPUWydPdZIwoqdhcbrpxmVZsJ0RIjtSPgKBkGFjLL28KTS+JmvMDUdpW8qi+ssr+8661gVuvB5BEp2g+hblb7FGtDJxpYoOumh9zgEqeNiUQ0PcwuRbMq6I0LTWtpA2USlM8Lh+TqfQS/2yL/HioZSby6Uvql+7pOUqwgEzOyeFWepkPtawcasgF6tXIT0b/U6ro9xg4x37+znYjiQCdSMmuiE6ipuBjWc9Zj3sYITVIpOqUTmPoZQ+Jg143/Mjj7Lx4EAnYwJzF6rgrgN20Ep+LCWJ8uPfirK7xz3mq3I5AC1Mv04lASX++vlzFJDXpX2cjeMTQfKQRdzBCEUxn87wP7IeKupOmuBRCIN9kx/cTHkjW8LW2cWQ4xlWKYE0FrvT7k853U0mtz5DlCo3UsQdgp4OKPdwzHDK+/hOjMBwdIeRSZZhLlkVK7v1GesJBrPX5u9dsXjW4jrexeRKntXJ4LBA+bGFjiBKNZci2j85ZC+tQ+TFmBAF/KFyM3U+AU/YzfbGvo/owKL46DIpw8XPeGBBN7mYbp7F7C3RpS67ex4d93FhUo1+c8nFKpfK9crLuJCg3zW/YjVPfhDb7l+i+ckQ2CyIQOffAtrJK1bgq1g7SokgtVbfIXRkhXCB/eGuCDmELuOaPkBpvtHrOy5R2OA/UaB3tkT9q2scWRO94H20Xz5+hUwMnbnnYMXBdhbIGSkisYnL+0CjYyh5NpvpFxczZI4i5N4EFwGGqbBRokVa7fZCyrQRcCnM1UX4F+NGSwebEFoJRcJibCU8pHXEj176TT+ZwJw6h23WZmdFaiZl7x80tLISYPUzkc3phG6ytWzCtY5LokIzi4IUQd8lnqa64tnWljx45BJq7te16Q4TAi9sUy2PeB/Cug3nThTYJOM2VRBhh5BZFZEu/NuCMRoL+/t8GWXgmlHPyXX7hC+EG3P6RaAUmXeOt3bcEY3EbR1lz/+20/y32YBh3nwWWJqwoCwqdymDHN8VqVbJqRkVs5vlVuhJFROR1oh1M3RE7KPtQsxOUkihnieftQqmolq2fNOA/1HRcOmczisgAt02cf7wkQEHtXUpXloTrWUu44eZo++HAt4bPehLE0Y4ojeu174WWjVXEf2o8JGjTj7JVKrKGVwDwHfbbZJhezyYtmApwP70ZJjXxjGmGzW8OoPDS2pBBE+FGyI2drwWqZ5j5EVyHXRqnMcDhS1Sd/WQ53lreedRO+QcvGDUE1mK8j3pYKWO20prGjlYzIEQXJGmxfYVi5IF4hNVNtQ16SiQaHuGwcumkQIxIEbDBFaynj2SBJmwuK/DF/wglKlIassIqgWrFKkk2S04SdUdv7/PFUxbc5u1x/oaluRcDWUnClAPnK52RdxEFr1l0ht6deyEQWokylCNpnBAntmV8muMCdTRYz2qDJC2JBuab3RfCqsXhHzmyVLEHDz7S/xClZVZx01CxqbFTv0x08wWTrDHbtOmBsvAhlfZzTbEyu8SHOb2oMYwilzSHYNndJKV8bctcNUm/54sKzQkkU2xmz8sbrpAofFHEvWNmQ3qRc6XIuU4OJoKgNHnJtwx7/En0MlM/cqVs7yYxKJ3VBtafHJM9h0cs9ZLD1qw7v0nyTPCY13hyTM9N20Bh39SN1gTPg+kZZwhpGRBzi4/LiR96QokFsxCgVOeIeOWuvG/iG53BM6unybZeenKv6OD+w/f2zLhP5ATK9YMQCGh15PHwLUkDJP05dKPwtinU/ywiBdHU3Pr5ZyZN5s/pp5VWh2LI032demuWIWSXirQXc++amCXX1xSKgl4wr/qGBjqg/Qsvo/e2hKwhkWZ+WrkrZ1fvwv27neTH1pVi0FHtNzY9Tp4lJLxmwB126MMmhoQXCF+f/4HOTMa3uJh/htwTOE7eN5yzAEZGX6RyiO8tdlY3B6LBPFEnou9Ha0Nyemw1hhvkdCTydcUGXQ0wEyU+4Sp8YUZAV4x0JFB/0WeNaEDmugCFajknjNDN2QYMMNaATM3jYuVD1zcoYLQKYb9RZbAznXTUGqQFb6RtrSStERCsEKm1/ovf9KiuqYb1ItGOXFQbpcQRXguWHpF5c39ncKmyoIqPIbjCS5DWaNkq+rdUMv5K8KPurY4bpFFli9ytDQD7PFZ6uxeWH9lu6HzS6uzvuSGvx8VQaGyjP5lJZtrFxnj6K4Ev6duvMafJnrzhIUpl6FimmW3JOjTQIKobyW/hhQHxDVf1zDq0m/UEvXoUVVMiFg9QELCd2pNpgGcc2aSeIsc5vMdnMMBcTfLdKs7FAYMFuKh2e5nJdhWUam97HbtOnzsT04B+EsRNbLyqgf+x54yN5/xxtg7N+cUQ8IZcOwk3+kGzmaq681wFQ6PnBNFhUOFKvAhC20EPXyANtTGFr6LvvxPfUmnXkTJE5hLqkgy4qZDgJrARfPOPe1mcwu/m8ttrxcEYso95nBMZblI0UC7bp7QT2xCdGvbi8Zwi0OVbshlVx6PDbDll1f0rEgxAoYUSEF3zrjW/vRk8njBKAt/vmmI0/aDHYZlnVJG4AbVQ+T4UAWCVgJJIuCRN4Owh4m92a8p4cgqB+3PKIWceyS0je4RfOjEpRql+VJrPx58qKJuXXW2aBWHay7QSsaPuseCuP3DKaUKYiLLl/Q7hCIhgImte5l7RKl2rlDE8i0A7/p7zT6rTP3+1jbEIeYyw2T33mq15hGKt/acUjsS++8lfLURcPU1vNpwg75Y0ry+fl1vGwBtwkqZRD8ZoBhL7iyxOwbL8iD97eO9tgvYDYhrJjjpfiuke4ReUu261YabAaS858VxZotuLlTT/g= + + + + +` + params := url.Values{} + params.Add("foo1", "bar1") + params.Add("SAMLResponse", samlp) + params.Add("foo2", "bar2") + url := "https://google.com/" + page := &mocks.Page{} + resp := &mocks.Response{} + req := &mocks.Request{} + regex, err := signinRegex() + assert.Nil(t, err) + page.Mock.On("Goto", url).Return(resp, nil) + page.Mock.On("WaitForRequest", regex).Return(req) + req.Mock.On("PostData").Return(params.Encode(), nil) + loginDetails := &creds.LoginDetails{ + URL: url, + } + samlResp, err := getSAMLResponse(page, loginDetails) + assert.Nil(t, err) + assert.Equal(t, samlp, samlResp) +} diff --git a/pkg/provider/f5apm/f5apm.go b/pkg/provider/f5apm/f5apm.go index a57a69555..39a64ea49 100644 --- a/pkg/provider/f5apm/f5apm.go +++ b/pkg/provider/f5apm/f5apm.go @@ -4,7 +4,7 @@ import ( "bytes" "encoding/base64" "fmt" - "io/ioutil" + "io" "net/http" "net/url" "strings" @@ -24,7 +24,7 @@ import ( var logger = logrus.WithField("provider", "f5apm") -//Client client for F5 APM +// Client client for F5 APM type Client struct { provider.ValidateBase @@ -126,7 +126,7 @@ func (ac *Client) getSAMLAssertion(loginDetails *creds.LoginDetails) (string, er return "", errors.Wrap(err, "Error retrieving SAML assertion request") } debugHTTPResponse(ac, res) - samlData, err := ioutil.ReadAll(res.Body) + samlData, err := io.ReadAll(res.Body) if err != nil { return "", errors.Wrap(err, "Error reading SAML assertion body") } @@ -211,7 +211,7 @@ func (ac *Client) postLoginForm(loginDetails *creds.LoginDetails, authForm url.V } debugHTTPResponse(ac, res) - data, err := ioutil.ReadAll(res.Body) + data, err := io.ReadAll(res.Body) if err != nil { return nil, errors.Wrap(err, "Error reading response body") } diff --git a/pkg/provider/f5apm/f5apm_test.go b/pkg/provider/f5apm/f5apm_test.go index 5aa065e0f..86ddf879e 100644 --- a/pkg/provider/f5apm/f5apm_test.go +++ b/pkg/provider/f5apm/f5apm_test.go @@ -2,11 +2,11 @@ package f5apm import ( "bytes" - "io/ioutil" "net/http" "net/http/cookiejar" "net/http/httptest" "net/url" + "os" "testing" "github.com/PuerkitoBio/goquery" @@ -18,7 +18,7 @@ import ( ) func TestClient_getLoginForm(t *testing.T) { - data, err := ioutil.ReadFile("example/loginpage.html") + data, err := os.ReadFile("example/loginpage.html") require.Nil(t, err) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -43,7 +43,7 @@ func TestClient_getLoginForm(t *testing.T) { }, authForm) } func TestClient_postLoginForm_user_pass(t *testing.T) { - data, err := ioutil.ReadFile("example/loginpage.html") + data, err := os.ReadFile("example/loginpage.html") require.Nil(t, err) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -68,7 +68,7 @@ func TestClient_postLoginForm_user_pass(t *testing.T) { } func TestClient_containsMFAForm(t *testing.T) { - data, err := ioutil.ReadFile("example/mfapage.html") + data, err := os.ReadFile("example/mfapage.html") require.Nil(t, err) doc, err := goquery.NewDocumentFromReader(bytes.NewReader(data)) require.Nil(t, err) @@ -78,7 +78,7 @@ func TestClient_containsMFAForm(t *testing.T) { } func TestClient_containsMFAForm_False(t *testing.T) { - data, err := ioutil.ReadFile("example/loginpage.html") + data, err := os.ReadFile("example/loginpage.html") require.Nil(t, err) doc, err := goquery.NewDocumentFromReader(bytes.NewReader(data)) require.Nil(t, err) diff --git a/pkg/provider/googleapps/README.md b/pkg/provider/googleapps/README.md index ab2646c0d..743aec050 100644 --- a/pkg/provider/googleapps/README.md +++ b/pkg/provider/googleapps/README.md @@ -4,10 +4,9 @@ This provider uses SAML with Google Apps to enable authentication of users to AW # prerequisites -Setup your Google Apps and AWS Account as per one of the configuration guides. +Setup your Google Workspace Apps and AWS Account: -* [How to Set Up Federated Single Sign-On to AWS Using Google Apps](https://aws.amazon.com/blogs/security/how-to-set-up-federated-single-sign-on-to-aws-using-google-apps/) -* [Using Google Apps SAML SSO to do one-click login to AWS](https://blog.faisalmisle.com/2015/11/using-google-apps-saml-sso-to-do-one-click-login-to-aws/) +* [How to set up IAM federation using Google Workspace](https://aws.amazon.com/blogs/security/how-to-set-up-federated-single-sign-on-to-aws-using-google-workspace/) # configuration diff --git a/pkg/provider/googleapps/example/challenge-extra-number.html b/pkg/provider/googleapps/example/challenge-extra-number.html new file mode 100644 index 000000000..67dcbc6b5 --- /dev/null +++ b/pkg/provider/googleapps/example/challenge-extra-number.html @@ -0,0 +1,96 @@ + + + + + + Google Accounts + + + +
+
+
+
+
+
+
+
+
+

2-Step Verification

+

This extra step shows it’s really you trying to sign in

+
+
+
+
Open the Gmail app on Nicholas’s iPhone
Google sent a notification to your Nicholas’s iPhone. Open the Gmail app, tap Yes on the prompt, then tap 89 on your phone to verify it’s you.
89

After you’ve finished on your phone, press the button below.

Don’t ask again on this device
+
+
+
+
+
+
+
nick@example.comUse a different account
+
+
+
+ +
+
+
+
+
+ + diff --git a/pkg/provider/googleapps/example/form-password-challengeid-1.html b/pkg/provider/googleapps/example/form-password-challengeid-1.html new file mode 100644 index 000000000..e8771fd7f --- /dev/null +++ b/pkg/provider/googleapps/example/form-password-challengeid-1.html @@ -0,0 +1,115 @@ + + + + + + + + + + Google Accounts + + +
+
+
+
+
+
+
+
+
+
+

One account. All of Google.

+

Sign in with your Google Account

+
+
+
+
+ + + + + + + + + + + + + + + +
+
+ + test-id1@example.com +
+ +
+ + +
+ +
+ + Stay signed in + +
+
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+
+
+

Sign in with a different account

+

One Google Account for everything Google

+
+
+
+
+
+
+ +
+ + + +
+
+
+
+ + +
diff --git a/pkg/provider/googleapps/example/form-password-challengeid-2.html b/pkg/provider/googleapps/example/form-password-challengeid-2.html new file mode 100644 index 000000000..046c73c1e --- /dev/null +++ b/pkg/provider/googleapps/example/form-password-challengeid-2.html @@ -0,0 +1,115 @@ + + + + + + + + + + Google Accounts + + +
+
+
+
+
+
+
+
+
+
+

One account. All of Google.

+

Sign in with your Google Account

+
+
+
+
+ + + + + + + + + + + + + + + +
+
+ + test-id2@example.com +
+ +
+ + +
+ +
+ + Stay signed in + +
+
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+
+
+

Sign in with a different account

+

One Google Account for everything Google

+
+
+
+
+
+
+ +
+ + + +
+
+
+
+ + +
diff --git a/pkg/provider/googleapps/googleapps.go b/pkg/provider/googleapps/googleapps.go index 7e8e6ad1f..83a663f3b 100644 --- a/pkg/provider/googleapps/googleapps.go +++ b/pkg/provider/googleapps/googleapps.go @@ -55,8 +55,12 @@ func (kc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) return "", errors.Wrap(err, "error loading first page") } + // Google supports only JavaScript-enabled clients + authForm.Set("bgresponse", "js_enabled") + authForm.Set("Email", loginDetails.Username) + // Post email address w/o password, then Get the password-input page passwordURL, passwordForm, err := kc.loadLoginPage(authURL+"?hl=en&loc=US", loginDetails.URL+"&hl=en&loc=US", authForm) if err != nil { return "", errors.Wrap(err, "error loading login page") @@ -64,23 +68,12 @@ func (kc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) logger.Debugf("loginURL: %s", passwordURL) - authForm.Set("Passwd", loginDetails.Password) + passwordForm.Set("Passwd", loginDetails.Password) + passwordForm.Set("TrustDevice", "on") referingURL := passwordURL - if _, rawIdPresent := passwordForm["rawidentifier"]; rawIdPresent { - authForm.Set("rawidentifier", loginDetails.Username) - referingURL = authURL - } - - if v, tlPresent := passwordForm["TL"]; tlPresent { - authForm.Set("TL", v[0]) - } - if v, gxfPresent := passwordForm["gxf"]; gxfPresent { - authForm.Set("gxf", v[0]) - } - - responseDoc, err := kc.loadChallengePage(passwordURL+"?hl=en&loc=US", referingURL, authForm, loginDetails) + responseDoc, err := kc.loadChallengePage(passwordURL+"?hl=en&loc=US", referingURL, passwordForm, loginDetails) if err != nil { return "", errors.Wrap(err, "error loading challenge page") } @@ -231,62 +224,7 @@ func (kc *Client) loadFirstPage(loginDetails *creds.LoginDetails) (string, url.V return "", nil, errors.Wrap(err, "failed to build login form data") } - _, loginPageV1 := authForm["GALX"] - - var postForm url.Values - // using a field which is known to be in the original login page - if loginPageV1 { - // Login page v1 - postForm = url.Values{ - "bgresponse": []string{"js_enabled"}, - "checkConnection": []string{""}, - "checkedDomains": []string{"youtube"}, - "continue": []string{authForm.Get("continue")}, - "gxf": []string{authForm.Get("gxf")}, - "identifier-captcha-input": []string{""}, - "identifiertoken": []string{""}, - "identifiertoken_audio": []string{""}, - "ltmpl": []string{"popup"}, - "oauth": []string{"1"}, - "Page": []string{authForm.Get("Page")}, - "Passwd": []string{""}, - "PersistentCookie": []string{"yes"}, - "ProfileInformation": []string{""}, - "pstMsg": []string{"0"}, - "sarp": []string{"1"}, - "scc": []string{"1"}, - "SessionState": []string{authForm.Get("SessionState")}, - "signIn": []string{authForm.Get("signIn")}, - "_utf8": []string{authForm.Get("_utf8")}, - "GALX": []string{authForm.Get("GALX")}, - } - } else { - // Login page v2 - postForm = url.Values{ - "challengeId": []string{"1"}, - "challengeType": []string{"1"}, - "continue": []string{authForm.Get("continue")}, - "scc": []string{"1"}, - "sarp": []string{"1"}, - "checkeddomains": []string{"youtube"}, - "checkConnection": []string{"youtube:930:1"}, - "pstMessage": []string{"1"}, - "oauth": []string{authForm.Get("oauth")}, - "flowName": []string{authForm.Get("flowName")}, - "faa": []string{"1"}, - "Email": []string{""}, - "Passwd": []string{""}, - "TrustDevice": []string{"on"}, - "bgresponse": []string{"js_enabled"}, - } - for _, k := range []string{"TL", "gxf"} { - if v, ok := authForm[k]; ok { - postForm.Set(k, v[0]) - } - } - } - - return submitURL, postForm, err + return submitURL, authForm, err } func (kc *Client) loadLoginPage(submitURL string, referer string, authForm url.Values) (string, url.Values, error) { @@ -442,7 +380,14 @@ func (kc *Client) loadChallengePage(submitURL string, referer string, authForm u return kc.loadResponsePage(secondActionURL, submitURL, responseForm) case strings.Contains(secondActionURL, "challenge/dp/"): // handle device push challenge - log.Print("Check your phone - after you have confirmed response press ENTER to continue.") + if extraNumber := extractDevicePushExtraNumber(doc); extraNumber != "" { + log.Println("Check your phone and tap 'Yes' on the prompt, then tap the number:") + log.Printf("\t%v\n", extraNumber) + log.Println("Then press ENTER to continue.") + } else { + log.Print("Check your phone and tap 'Yes' on the prompt. Then press ENTER to continue.") + } + _, err := bufio.NewReader(os.Stdin).ReadBytes('\n') if err != nil { return nil, errors.Wrap(err, "error reading new line \\n") @@ -799,3 +744,11 @@ func isAppId(val string) string { } return appId } + +func extractDevicePushExtraNumber(doc *goquery.Document) string { + extraNumber := "" + doc.Find("div[jsname=feLNVc]").Each(func(_ int, s *goquery.Selection) { + extraNumber = s.Text() + }) + return extraNumber +} diff --git a/pkg/provider/googleapps/googleapps_test.go b/pkg/provider/googleapps/googleapps_test.go index 8f9d314ec..01d2ac27e 100644 --- a/pkg/provider/googleapps/googleapps_test.go +++ b/pkg/provider/googleapps/googleapps_test.go @@ -2,10 +2,10 @@ package googleapps import ( "bytes" - "io/ioutil" "net/http" "net/http/httptest" "net/url" + "os" "strings" "testing" @@ -77,9 +77,63 @@ func TestContentContainsMessage2(t *testing.T) { require.Equal(t, "This extra step shows that it’s really you trying to sign in", txt) } +func TestPasswordFormChallengeId1(t *testing.T) { + data, err := os.ReadFile("example/form-password-challengeid-1.html") + require.Nil(t, err) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write(data) + })) + defer ts.Close() + + opts := &provider.HTTPClientOptions{IsWithRetries: false} + kc := Client{client: &provider.HTTPClient{Client: http.Client{}, Options: opts}} + loginDetails := &creds.LoginDetails{URL: ts.URL, Username: "test-id1@example.com", Password: "test123"} + + authForm := url.Values{} + authForm.Set("bgresponse", "js_enabled") + authForm.Set("Email", loginDetails.Username) + + passwordURL, passwordForm, err := kc.loadLoginPage(ts.URL, loginDetails.URL+"&hl=en&loc=US", authForm) + require.Nil(t, err) + require.NotEmpty(t, passwordURL) + require.Equal(t, "1", passwordForm.Get("challengeId")) + // check pre-filled email + require.NotEmpty(t, passwordForm.Get("Email")) + // check password form + require.Empty(t, passwordForm.Get("Passwd")) +} + +func TestPasswordFormChallengeId2(t *testing.T) { + data, err := os.ReadFile("example/form-password-challengeid-2.html") + require.Nil(t, err) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write(data) + })) + defer ts.Close() + + opts := &provider.HTTPClientOptions{IsWithRetries: false} + kc := Client{client: &provider.HTTPClient{Client: http.Client{}, Options: opts}} + loginDetails := &creds.LoginDetails{URL: ts.URL, Username: "test-id2@example.com", Password: "test123"} + + authForm := url.Values{} + authForm.Set("bgresponse", "js_enabled") + authForm.Set("Email", loginDetails.Username) + + passwordURL, passwordForm, err := kc.loadLoginPage(ts.URL, loginDetails.URL+"&hl=en&loc=US", authForm) + require.Nil(t, err) + require.NotEmpty(t, passwordURL) + require.Equal(t, "2", passwordForm.Get("challengeId")) + // check pre-filled email + require.NotEmpty(t, passwordForm.Get("Email")) + // check password form + require.Empty(t, passwordForm.Get("Passwd")) +} + func TestChallengePage(t *testing.T) { - data, err := ioutil.ReadFile("example/challenge-totp.html") + data, err := os.ReadFile("example/challenge-totp.html") require.Nil(t, err) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -98,7 +152,7 @@ func TestChallengePage(t *testing.T) { } func TestExtractDataAttributes(t *testing.T) { - data, err := ioutil.ReadFile("example/challenge-prompt.html") + data, err := os.ReadFile("example/challenge-prompt.html") require.Nil(t, err) doc, err := goquery.NewDocumentFromReader(bytes.NewReader(data)) require.Nil(t, err) @@ -117,3 +171,19 @@ func TestWrongPassword(t *testing.T) { txt := doc.Selection.Find("#" + passwordErrorId).Text() require.NotEqual(t, "", txt) } + +func TestExtractDevicePushExtraNumber(t *testing.T) { + data1, err := os.ReadFile("example/challenge-extra-number.html") + require.Nil(t, err) + doc1, err := goquery.NewDocumentFromReader(bytes.NewReader(data1)) + require.Nil(t, err) + require.Equal(t, "89", extractDevicePushExtraNumber(doc1)) + + for _, filename := range []string{"example/challenge-prompt.html", "example/challenge-totp.html"} { + data2, err := os.ReadFile(filename) + require.Nil(t, err) + doc2, err := goquery.NewDocumentFromReader(bytes.NewReader(data2)) + require.Nil(t, err) + require.Equal(t, "", extractDevicePushExtraNumber(doc2)) + } +} diff --git a/pkg/provider/http_test.go b/pkg/provider/http_test.go index 4cfe6c6e2..ee16b5216 100644 --- a/pkg/provider/http_test.go +++ b/pkg/provider/http_test.go @@ -49,8 +49,8 @@ func TestClientDisableRedirect(t *testing.T) { require.Nil(t, err) res, err := hc.Do(req) - require.Error(t, err) - require.Nil(t, res) + require.Nil(t, err) + require.Equal(t, 302, res.StatusCode) } func TestClientDoResponseCheck(t *testing.T) { diff --git a/pkg/provider/jumpcloud/jumpcloud.go b/pkg/provider/jumpcloud/jumpcloud.go index 6f2484eb9..2f7723254 100644 --- a/pkg/provider/jumpcloud/jumpcloud.go +++ b/pkg/provider/jumpcloud/jumpcloud.go @@ -5,7 +5,7 @@ import ( "encoding/json" "fmt" "html" - "io/ioutil" + "io" "log" "net/http" "net/url" @@ -106,7 +106,7 @@ func (jc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) defer res.Body.Close() // Grab the web response that has the xsrf in it - xsrfBody, err := ioutil.ReadAll(res.Body) + xsrfBody, err := io.ReadAll(res.Body) if err != nil { return samlAssertion, errors.Wrap(err, "error reading body of XSRF response") } @@ -151,7 +151,7 @@ func (jc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) if res.StatusCode == 401 { // Grab the body from the response that has the message in it. - messageBody, err := ioutil.ReadAll(res.Body) + messageBody, err := io.ReadAll(res.Body) if err != nil { return samlAssertion, errors.Wrap(err, "Error reading body") } @@ -182,7 +182,7 @@ func (jc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) // Check if our auth was successful if res.StatusCode == 200 { // Grab the body from the response that has the redirect in it. - reDirBody, err := ioutil.ReadAll(res.Body) + reDirBody, err := io.ReadAll(res.Body) if err != nil { return samlAssertion, errors.Wrap(err, "Error reading body") } @@ -271,7 +271,7 @@ func (jc *Client) verifyMFA(jumpCloudOrgHost string, loginDetails *creds.LoginDe } defer res.Body.Close() - respBody, err := ioutil.ReadAll(res.Body) + respBody, err := io.ReadAll(res.Body) if err != nil { return nil, err } @@ -339,7 +339,7 @@ func (jc *Client) verifyMFA(jumpCloudOrgHost string, loginDetails *creds.LoginDe if res.StatusCode != 200 { return nil, errors.New("error retrieving Duo configuration, non 200 status returned") } - duoResp, err := ioutil.ReadAll(res.Body) + duoResp, err := io.ReadAll(res.Body) if err != nil { return nil, errors.Wrap(err, "error retrieving Duo configuration") } @@ -436,7 +436,7 @@ func (jc *Client) verifyMFA(jumpCloudOrgHost string, loginDetails *creds.LoginDe } defer res.Body.Close() - body, err := ioutil.ReadAll(res.Body) + body, err := io.ReadAll(res.Body) if err != nil { return nil, errors.Wrap(err, "error retrieving body from response") } @@ -469,7 +469,7 @@ func (jc *Client) verifyMFA(jumpCloudOrgHost string, loginDetails *creds.LoginDe } defer res.Body.Close() - body, err = ioutil.ReadAll(res.Body) + body, err = io.ReadAll(res.Body) if err != nil { return nil, errors.Wrap(err, "error retrieving body from response") } @@ -503,7 +503,7 @@ func (jc *Client) verifyMFA(jumpCloudOrgHost string, loginDetails *creds.LoginDe } defer res.Body.Close() - body, err = ioutil.ReadAll(res.Body) + body, err = io.ReadAll(res.Body) if err != nil { return nil, errors.Wrap(err, "error retrieving body from response") } @@ -547,7 +547,7 @@ func (jc *Client) verifyMFA(jumpCloudOrgHost string, loginDetails *creds.LoginDe } defer res.Body.Close() - body, err = ioutil.ReadAll(res.Body) + body, err = io.ReadAll(res.Body) if err != nil { return nil, errors.Wrap(err, "duoResultSubmit: error retrieving body from response") } diff --git a/pkg/provider/jumpcloud/jumpcloud_protect.go b/pkg/provider/jumpcloud/jumpcloud_protect.go index 7becc0185..1f8997e8d 100644 --- a/pkg/provider/jumpcloud/jumpcloud_protect.go +++ b/pkg/provider/jumpcloud/jumpcloud_protect.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "net/http" "net/url" "path" @@ -43,7 +42,7 @@ func (jc *Client) jumpCloudProtectAuth(submitUrl string, xsrfToken string) (*htt return nil, errors.New("error retrieving JumpCloud PUSH payload, non 200 status returned") } - jpResp, err := ioutil.ReadAll(res.Body) + jpResp, err := io.ReadAll(res.Body) if err != nil { return nil, errors.Wrap(err, "error retrieving JumpCloud PUSH payload") } diff --git a/pkg/provider/keycloak/keycloak.go b/pkg/provider/keycloak/keycloak.go index faebc79fe..aa82e88b4 100644 --- a/pkg/provider/keycloak/keycloak.go +++ b/pkg/provider/keycloak/keycloak.go @@ -4,7 +4,7 @@ import ( "bytes" "encoding/base64" "fmt" - "io/ioutil" + "io" "log" "net/http" "net/url" @@ -193,7 +193,7 @@ func (kc *Client) postLoginForm(authSubmitURL string, authForm url.Values) ([]by return nil, errors.Wrap(err, "error retrieving login form") } - data, err := ioutil.ReadAll(res.Body) + data, err := io.ReadAll(res.Body) if err != nil { return nil, errors.Wrap(err, "error retrieving body") } diff --git a/pkg/provider/keycloak/keycloak_test.go b/pkg/provider/keycloak/keycloak_test.go index 0f5151bd5..60a3f9894 100644 --- a/pkg/provider/keycloak/keycloak_test.go +++ b/pkg/provider/keycloak/keycloak_test.go @@ -2,10 +2,10 @@ package keycloak import ( "bytes" - "io/ioutil" "net/http" "net/http/httptest" "net/url" + "os" "testing" "github.com/PuerkitoBio/goquery" @@ -23,7 +23,7 @@ const ( func TestClient_getLoginForm(t *testing.T) { - data, err := ioutil.ReadFile("example/loginpage.html") + data, err := os.ReadFile("example/loginpage.html") require.Nil(t, err) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -47,10 +47,10 @@ func TestClient_getLoginForm(t *testing.T) { func TestClient_getLoginFormRedirect(t *testing.T) { - redirectData, err := ioutil.ReadFile("example/redirect.html") + redirectData, err := os.ReadFile("example/redirect.html") require.Nil(t, err) - data, err := ioutil.ReadFile("example/loginpage.html") + data, err := os.ReadFile("example/loginpage.html") require.Nil(t, err) count := 0 @@ -82,7 +82,7 @@ func TestClient_getLoginFormRedirect(t *testing.T) { func TestClient_postLoginForm(t *testing.T) { - data, err := ioutil.ReadFile("example/mfapage.html") + data, err := os.ReadFile("example/mfapage.html") require.Nil(t, err) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -106,7 +106,7 @@ func TestClient_postLoginForm(t *testing.T) { func TestClient_postTotpForm(t *testing.T) { - data, err := ioutil.ReadFile("example/assertion.html") + data, err := os.ReadFile("example/assertion.html") require.Nil(t, err) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -114,7 +114,7 @@ func TestClient_postTotpForm(t *testing.T) { })) defer ts.Close() - mfapage, err := ioutil.ReadFile("example/mfapage.html") + mfapage, err := os.ReadFile("example/mfapage.html") require.Nil(t, err) doc, err := goquery.NewDocumentFromReader(bytes.NewReader(mfapage)) require.Nil(t, err) @@ -138,7 +138,7 @@ func TestClient_postTotpForm(t *testing.T) { func TestClient_postTotpFormWithProvidedMFAToken(t *testing.T) { - data, err := ioutil.ReadFile("example/assertion.html") + data, err := os.ReadFile("example/assertion.html") require.Nil(t, err) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -146,7 +146,7 @@ func TestClient_postTotpFormWithProvidedMFAToken(t *testing.T) { })) defer ts.Close() - mfapage, err := ioutil.ReadFile("example/mfapage.html") + mfapage, err := os.ReadFile("example/mfapage.html") require.Nil(t, err) doc, err := goquery.NewDocumentFromReader(bytes.NewReader(mfapage)) require.Nil(t, err) @@ -167,7 +167,7 @@ func TestClient_postTotpFormWithProvidedMFAToken(t *testing.T) { } func TestClient_postTotpFormWithMultipleAuthenticators(t *testing.T) { - data, err := ioutil.ReadFile("example/assertion.html") + data, err := os.ReadFile("example/assertion.html") require.Nil(t, err) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -175,7 +175,7 @@ func TestClient_postTotpFormWithMultipleAuthenticators(t *testing.T) { })) defer ts.Close() - mfapage, err := ioutil.ReadFile("example/mfapage2authenticators.html") + mfapage, err := os.ReadFile("example/mfapage2authenticators.html") require.Nil(t, err) doc, err := goquery.NewDocumentFromReader(bytes.NewReader(mfapage)) require.Nil(t, err) @@ -203,7 +203,7 @@ func TestClient_postTotpFormWithMultipleAuthenticators(t *testing.T) { } func TestClient_extractSamlResponse(t *testing.T) { - data, err := ioutil.ReadFile("example/assertion.html") + data, err := os.ReadFile("example/assertion.html") require.Nil(t, err) doc, err := goquery.NewDocumentFromReader(bytes.NewReader(data)) @@ -215,7 +215,7 @@ func TestClient_extractSamlResponse(t *testing.T) { } func TestClient_containsTotpForm(t *testing.T) { - data, err := ioutil.ReadFile("example/mfapage.html") + data, err := os.ReadFile("example/mfapage.html") require.Nil(t, err) doc, err := goquery.NewDocumentFromReader(bytes.NewReader(data)) @@ -225,7 +225,7 @@ func TestClient_containsTotpForm(t *testing.T) { } func TestClient_extractWebauthnParameters(t *testing.T) { - data, err := ioutil.ReadFile("example/webauthnPage.html") + data, err := os.ReadFile("example/webauthnPage.html") require.Nil(t, err) doc, err := goquery.NewDocumentFromReader(bytes.NewReader(data)) diff --git a/pkg/provider/netiq/netiq_test.go b/pkg/provider/netiq/netiq_test.go index 6dddc201a..33ebf00d7 100644 --- a/pkg/provider/netiq/netiq_test.go +++ b/pkg/provider/netiq/netiq_test.go @@ -2,8 +2,8 @@ package netiq import ( "bytes" - "io/ioutil" "net/url" + "os" "testing" "github.com/PuerkitoBio/goquery" @@ -13,7 +13,7 @@ import ( func TestIsSAMLResponsePositive(t *testing.T) { //given - samlResponseData, err := ioutil.ReadFile("responses/samlRespose.html") + samlResponseData, err := os.ReadFile("responses/samlRespose.html") require.Nil(t, err) //when @@ -26,7 +26,7 @@ func TestIsSAMLResponsePositive(t *testing.T) { func TestIsSAMLResponseNegative(t *testing.T) { //given - getToContentData, err := ioutil.ReadFile("responses/getToContent.html") + getToContentData, err := os.ReadFile("responses/getToContent.html") require.Nil(t, err) //when @@ -39,7 +39,7 @@ func TestIsSAMLResponseNegative(t *testing.T) { func TestExtractSAMLAssertion(t *testing.T) { //given - samlResponseData, err := ioutil.ReadFile("responses/samlRespose.html") + samlResponseData, err := os.ReadFile("responses/samlRespose.html") require.Nil(t, err) expectedResult := "PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIERlc3RpbmF0aW9uPSJodHRwczovL3NpZ25pbi5hd3MuYW1hem9uLmNvbS9zYW1sIiBJRD0iaWRteFVmZUR5dVJhODlaTWtEOUg3Z3pKQl9tUTQiIElzc3VlSW5zdGFudD0iMjAyMC0wMy0wNVQwMjowNToxOFoiIFZlcnNpb249IjIuMCI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vbG9naW4uYXV0aGJyaWRnZS53ZXN0cGFjZ3JvdXAuY29tL25pZHAvc2FtbDIvbWV0YWRhdGE8L3NhbWw6SXNzdWVyPjxzYW1scDpTdGF0dXM+PHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPjwvc2FtbHA6U3RhdHVzPjxzYW1sOkFzc2VydGlvbiBJRD0iaWRpWmdNQTlZUlBGYURBSjdWaGZnTHpsckFLWjQiIElzc3VlSW5zdGFudD0iMjAyMC0wMy0wNVQwMjowNToxOFoiIFZlcnNpb249IjIuMCI+PHNhbWw6SXNzdWVyPmh0dHBzOi8vbG9naW4uYXV0aGJyaWRnZS53ZXN0cGFjZ3JvdXAuY29tL25pZHAvc2FtbDIvbWV0YWRhdGE8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjxkczpTaWduZWRJbmZvPjxDYW5vbmljYWxpemF0aW9uTWV0aG9kIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIiBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+PGRzOlJlZmVyZW5jZSBVUkk9IiNpZGlaZ01BOVlSUEZhREFKN1ZoZmdMemxyQUtaNCI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjc2hhMjU2Ii8+PERpZ2VzdFZhbHVlIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj45aDNZNUdxQlpvRFZ4K3czU2t0VmhKYzd5YXF3SU5FSFljRVlZWCt2RVMwPTwvRGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxTaWduYXR1cmVWYWx1ZSB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+CmhZcUxFWVU3Wmc2eUFHZVRwVzNUSGJDTzlrNTRNaU1iRC95NmZ5ZW9SYThtbUxQYnpxaUtYR1NlQUMxT0t5UnRvanNLQStCbVpCTzQKQkl1bDRtajM1KzY4d3luZU9Gc3pSOVYyMlphSWJnenh2Ny8wR1FNL0s2ZEJpakdGc1NxcXZONUlBTi9DZDhJUDU3N1pyM2dNQ1ZqcApnQ0k4dTlQUktOclJHdmhRUmc4OW42bXdLcml4UnZkVHh5WXArT3R1VGh4NHkrcytkOGYyNEhxOVZ0M1AwckljSWJNelplMitWVmkzCmV5NVNDbnQzK3I1QVE4MHdhSHlPU3lld21ORHJNWHhrN3pWdW9XeVFrbFR3Y1ZQQUdZTDU1UXgrZG1mZ1FCU1JOZzN1VzM3T3czZkEKekZIc1I2eUhLdkc4dFRMWHVoVkp6am85OXV2MlF4Zi9nNFc3MGc9PQo8L1NpZ25hdHVyZVZhbHVlPjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPgpNSUlJcHpDQ0I0K2dBd0lCQWdJVEh3QUF2dWs4YUd1UzVzRkdYQUFBQUFDKzZUQU5CZ2txaGtpRzl3MEJBUXNGQURDQnBqRUxNQWtHCkExVUVCaE1DUVZVeEREQUtCZ05WQkFnVEEwNVRWekVQTUEwR0ExVUVCeE1HVTNsa2JtVjVNU1F3SWdZRFZRUUtFeHRYWlhOMGNHRmoKSUVKaGJtdHBibWNnUTI5eWNHOXlZWFJwYjI0eEx6QXRCZ05WQkFzVEprUnBaMmwwWVd3Z1EyVnlkR2xtYVdOaGRHVnpJRk5sWTNWeQphWFI1SUZObGNuWnBZMlZ6TVNFd0h3WURWUVFERXhoWFpYTjBjR0ZqSUZOSVFUSWdVMU5NSUVOQklGZFRSRU13SGhjTk1qQXdNakl3Ck1qQXlOVFU1V2hjTk1qSXdNakU1TWpBeU5UVTVXakNCdERFTE1Ba0dBMVVFQmhNQ1FWVXhHREFXQmdOVkJBZ1REMDVsZHlCVGIzVjAKYUNCWFlXeGxjekVQTUEwR0ExVUVCeE1HVTNsa2JtVjVNU1F3SWdZRFZRUUtFeHRYWlhOMGNHRmpJRUpoYm10cGJtY2dRMjl5Y0c5eQpZWFJwYjI0eEhUQWJCZ05WQkFzVEZGZENReUJKVTFOUUlFbEJUU0JRY205bmNtRnRNVFV3TXdZRFZRUURFeXh1WVcwdGMzTndMVUZYClV5MWhkWFJvWW5KcFpHZGxMWEJ5YjJRdWQyVnpkSEJoWTJkeWIzVndMbU52YlRDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVAKQURDQ0FRb0NnZ0VCQUpQMWtKQW81THgzWGRFRkIrcGxidE16dy9McUU3UnUzMnFXdkFVMUJ3YUVTQ2ZQak5KVW9Ga0RrS3NNeUE0WQpDcjYrcWhwcEQ0K0tlZC9tMjhmVTdmbWFVNm1hWUlsbFpsWnBnTFZqV3BNcDNQdVF1UnFtL0w4dlcvZHpURkdoOTZxb0pNR1paWWZ5CnYwaVVlM2wrdmpuNXFORjNBdERleDkrSWdHZUx3OHZ5L1NRRmFWeUlOOTNwYW4rM0hERDM0WFlTZmVidFBoQXppbGM4c3BtUmI3S2MKS2VsYmhDQjF4RVRaM29nODZCYzRJTzI5YXFVbXhZN01ybTVmQzdBcFpHYXVXWG55QWZEaGJkRlF3NGQ2elFOQWZpdGNKSk1CbW8rTgpJS1VpMzBBc0lCbHpxaVNNTTdNTFU3U3ZhcFN3QTdOTXNEWlhKR1FIMklqNVcyVWFuTDBDQXdFQUFhT0NCTHd3Z2dTNE1CMEdBMVVkCkRnUVdCQlJiblI4TnA3OHZKS05KY29ETWRVUGw1aDdYc3pBZkJnTlZIU01FR0RBV2dCUmRmdDhhVnM0RGhONFJiS1Zic3QrZUtOeUEKWmpDQ0FXY0dBMVVkSHdTQ0FWNHdnZ0ZhTUlJQlZxQ0NBVktnZ2dGT2htNW9kSFJ3T2k4dmQySmpZMkV1Y0d0cE1pNXpjbll1ZDJWegpkSEJoWXk1amIyMHVZWFV2UTBSUUwxZGxjM1J3WVdNbE1qQlRTRUV5SlRJd1UxTk1KVEl3UTBFbE1qQlhVMFJETDFkbGMzUndZV01sCk1qQlRTRUV5SlRJd1UxTk1KVEl3UTBFbE1qQlhVMFJETG1OeWJJYUIyMnhrWVhBNkx5OHZRMDQ5VjJWemRIQmhZeVV5TUZOSVFUSWwKTWpCVFUwd2xNakJEUVNVeU1GZFRSRU1zUTA0OVlYVXlNREEwYzNBeE1qWTFMRU5PUFVORVVDeERUajFRZFdKc2FXTWxNakJMWlhrbApNakJUWlhKMmFXTmxjeXhEVGoxVFpYSjJhV05sY3l4RFRqMURiMjVtYVdkMWNtRjBhVzl1TEVSRFBYZGlZMkYxTEVSRFBYZGxjM1J3CllXTXNSRU05WTI5dExFUkRQV0YxUDJObGNuUnBabWxqWVhSbFVtVjJiMk5oZEdsdmJreHBjM1EvWW1GelpUOXZZbXBsWTNSRGJHRnoKY3oxalVreEVhWE4wY21saWRYUnBiMjVRYjJsdWREQ0NBVjhHQ0NzR0FRVUZCd0VCQklJQlVUQ0NBVTB3ZWdZSUt3WUJCUVVITUFLRwpibWgwZEhBNkx5OTNZbU5qWVM1d2Eya3lMbk55ZGk1M1pYTjBjR0ZqTG1OdmJTNWhkUzlCU1VFdlYyVnpkSEJoWXlVeU1GTklRVElsCk1qQlRVMHdsTWpCRFFTVXlNRmRUUkVNdlYyVnpkSEJoWXlVeU1GTklRVElsTWpCVFUwd2xNakJEUVNVeU1GZFRSRU11WTNKME1JSE8KQmdnckJnRUZCUWN3QW9hQndXeGtZWEE2THk4dlEwNDlWMlZ6ZEhCaFl5VXlNRk5JUVRJbE1qQlRVMHdsTWpCRFFTVXlNRmRUUkVNcwpRMDQ5UVVsQkxFTk9QVkIxWW14cFl5VXlNRXRsZVNVeU1GTmxjblpwWTJWekxFTk9QVk5sY25acFkyVnpMRU5PUFVOdmJtWnBaM1Z5CllYUnBiMjRzUkVNOWQySmpZWFVzUkVNOWQyVnpkSEJoWXl4RVF6MWpiMjBzUkVNOVlYVS9ZMEZEWlhKMGFXWnBZMkYwWlQ5aVlYTmwKUDI5aWFtVmpkRU5zWVhOelBXTmxjblJwWm1sallYUnBiMjVCZFhSb2IzSnBkSGt3Q3dZRFZSMFBCQVFEQWdXZ01Ec0dDU3NHQVFRQgpnamNWQndRdU1Dd0dKQ3NHQVFRQmdqY1ZDSVhlc3lYZ2hRdUMwWTBwaDhyaGVzZjhFNEZZMStKS2hMcnBZUUlCWkFJQkNUQVRCZ05WCkhTVUVEREFLQmdnckJnRUZCUWNEQXpBYkJna3JCZ0VFQVlJM0ZRb0VEakFNTUFvR0NDc0dBUVVGQndNRE1JSUJLZ1lEVlIwZ0JJSUIKSVRDQ0FSMHdYUVlMS3dZQkJBR2NFNGRvQWdNd1RqQk1CZ2dyQmdFRkJRY0NBUlpBYUhSMGNEb3ZMM2RpWTJOaExuQnJhVEl1YzNKMgpMbmRsYzNSd1lXTXVZMjl0TG1GMUwxZGxjM1J3WVdOUWIyeHBZM2t2VjBKRFgwTlFVekl1Y0dSbUFEQmRCZ3dyQmdFRUFad1RoMmdCCkFRUXdUVEJMQmdnckJnRUZCUWNDQVJZL2FIUjBjRG92TDNkaVkyTmhMbkJyYVRJdWMzSjJMbmRsYzNSd1lXTXVZMjl0TG1GMUwxZGwKYzNSd1lXTlFiMnhwWTNrdlYwSkRYME5RTWk1d1pHWUFNRjBHRENzR0FRUUJuQk9IYUFFQkFUQk5NRXNHQ0NzR0FRVUZCd0lCRmo5bwpkSFJ3T2k4dmQySmpZMkV1Y0d0cExuTnlkaTUzWlhOMGNHRmpMbU52YlM1aGRTOVhaWE4wY0dGalVHOXNhV041TDFkQ1ExOUpWRk5RCkxuQmtaZ0F3RFFZSktvWklodmNOQVFFTEJRQURnZ0VCQUVPMnkwRFR3cGNuSDgvcURkVVpQK1JweGxvMm5sWVNhQmR3bnRBZWtuNkkKYXV5ZURhUEIxN2I3UkJVVzNNSU1zcWdMa2xSQWtKQTVsdTJMUy9YYldteXZvR2lGQnV4RTIxNktrbFNvWmlWeGRDR2p2M2ZJVGNpdgppVW9Mb1dQR2krSHZmelBEVXBIdkg5NUFBL0swOXBGM1ZCODJNRWc1dVNtTjkzMTQwdGp4eXFpQnJ5WEUvT2FRTmk3NEdoNWxWbU9iCkUzRnBnTEdSSGw2ZlFNeGx3UFRhYXpuZ0c5dTY2SU95ZWwvcVVwTkxzajlJY1U1U2VPTjJEMHg3bExVSWttcVM0cGloM2tSL3Q0OUUKQWdieDFEamw3akNIRVh6dzdxS3BCS1hGS2t1WkRLMDZ3R0laQmxsOGlLc2xQQ0E5MkZySllQbW05a1dTNzlXVm54WFNVUTQ9CjwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6dW5zcGVjaWZpZWQiIE5hbWVRdWFsaWZpZXI9Imh0dHBzOi8vbG9naW4uYXV0aGJyaWRnZS53ZXN0cGFjZ3JvdXAuY29tL25pZHAvc2FtbDIvbWV0YWRhdGEiIFNQTmFtZVF1YWxpZmllcj0idXJuOmFtYXpvbjp3ZWJzZXJ2aWNlcyI+TDEyMjg5Mzwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMC0wMy0wNVQwMjoxMDoxOFoiIFJlY2lwaWVudD0iaHR0cHM6Ly9zaWduaW4uYXdzLmFtYXpvbi5jb20vc2FtbCIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDIwLTAzLTA1VDAyOjAwOjE4WiIgTm90T25PckFmdGVyPSIyMDIwLTAzLTA1VDAyOjEwOjE4WiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT51cm46YW1hem9uOndlYnNlcnZpY2VzPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAyMC0wMy0wNVQwMTo0MToyNFoiIFNlc3Npb25JbmRleD0iaWRWTzRuRkVNbm9TNFJfemRaNVZNX1RlcGdmVGciPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpLZXJiZXJvczwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48c2FtbDpBdXRobkNvbnRleHREZWNsUmVmPmF3cy9tZmEvYXV0aC91cmk8L3NhbWw6QXV0aG5Db250ZXh0RGVjbFJlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgTmFtZT0iaHR0cHM6Ly9hd3MuYW1hem9uLmNvbS9TQU1ML0F0dHJpYnV0ZXMvUm9sZVNlc3Npb25OYW1lIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OnVuc3BlY2lmaWVkIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5MMTIyODkzPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgTmFtZT0iaHR0cHM6Ly9hd3MuYW1hem9uLmNvbS9TQU1ML0F0dHJpYnV0ZXMvUm9sZSIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDp1bnNwZWNpZmllZCI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+YXJuOmF3czppYW06Ojg2MzM2OTQzMDA1NTpyb2xlL3diYy1hZG1pbixhcm46YXdzOmlhbTo6ODYzMzY5NDMwMDU1OnNhbWwtcHJvdmlkZXIvd2VzdHBhY2lkcDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hcm46YXdzOmlhbTo6ODYzMzY5NDMwMDU1OnJvbGUvd2JjLWVuZ2luZWVyLGFybjphd3M6aWFtOjo4NjMzNjk0MzAwNTU6c2FtbC1wcm92aWRlci93ZXN0cGFjaWRwPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFybjphd3M6aWFtOjo4NjMzNjk0MzAwNTU6cm9sZS93YmMtbWFuYWdlcixhcm46YXdzOmlhbTo6ODYzMzY5NDMwMDU1OnNhbWwtcHJvdmlkZXIvd2VzdHBhY2lkcDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hcm46YXdzOmlhbTo6ODYzMzY5NDMwMDU1OnJvbGUvd2JjLXZpZXdvbmx5LGFybjphd3M6aWFtOjo4NjMzNjk0MzAwNTU6c2FtbC1wcm92aWRlci93ZXN0cGFjaWRwPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFybjphd3M6aWFtOjo4NjMzNjk0MzAwNTU6cm9sZS93YmMtcmVhZG9ubHksYXJuOmF3czppYW06Ojg2MzM2OTQzMDA1NTpzYW1sLXByb3ZpZGVyL3dlc3RwYWNpZHA8L3NhbWw6QXR0cmlidXRlVmFsdWU+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+YXJuOmF3czppYW06OjE2OTI2NzQ1MTI4NTpyb2xlL3diYy1hZG1pbixhcm46YXdzOmlhbTo6MTY5MjY3NDUxMjg1OnNhbWwtcHJvdmlkZXIvd2VzdHBhY2lkcDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hcm46YXdzOmlhbTo6MTY5MjY3NDUxMjg1OnJvbGUvd2JjLWVuZ2luZWVyLGFybjphd3M6aWFtOjoxNjkyNjc0NTEyODU6c2FtbC1wcm92aWRlci93ZXN0cGFjaWRwPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFybjphd3M6aWFtOjoxNjkyNjc0NTEyODU6cm9sZS93YmMtdmlld29ubHksYXJuOmF3czppYW06OjE2OTI2NzQ1MTI4NTpzYW1sLXByb3ZpZGVyL3dlc3RwYWNpZHA8L3NhbWw6QXR0cmlidXRlVmFsdWU+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+YXJuOmF3czppYW06OjQwMTI1MjgzMDY4Njpyb2xlL3diYy1hZG1pbixhcm46YXdzOmlhbTo6NDAxMjUyODMwNjg2OnNhbWwtcHJvdmlkZXIvd2VzdHBhY2lkcDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hcm46YXdzOmlhbTo6NDAxMjUyODMwNjg2OnJvbGUvd2JjLWVuZ2luZWVyLGFybjphd3M6aWFtOjo0MDEyNTI4MzA2ODY6c2FtbC1wcm92aWRlci93ZXN0cGFjaWRwPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFybjphd3M6aWFtOjo0MDEyNTI4MzA2ODY6cm9sZS93YmMtdmlld29ubHksYXJuOmF3czppYW06OjQwMTI1MjgzMDY4NjpzYW1sLXByb3ZpZGVyL3dlc3RwYWNpZHA8L3NhbWw6QXR0cmlidXRlVmFsdWU+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+YXJuOmF3czppYW06OjQwMTI1MjgzMDY4Njpyb2xlL3diYy1yZWFkb25seSxhcm46YXdzOmlhbTo6NDAxMjUyODMwNjg2OnNhbWwtcHJvdmlkZXIvd2VzdHBhY2lkcDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hcm46YXdzOmlhbTo6MTY5MjY3NDUxMjg1OnJvbGUvd2JjLXJlYWRvbmx5LGFybjphd3M6aWFtOjoxNjkyNjc0NTEyODU6c2FtbC1wcm92aWRlci93ZXN0cGFjaWRwPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+" @@ -55,7 +55,7 @@ func TestExtractSAMLAssertion(t *testing.T) { func TestExtractGetToContentUrlPositive(t *testing.T) { //given - getToContentData, err := ioutil.ReadFile("responses/getToContent.html") + getToContentData, err := os.ReadFile("responses/getToContent.html") require.Nil(t, err) expectedResourceUrl := "/nidp/jsp/content.jsp?sid=0&option=credential&id=AWS" @@ -71,7 +71,7 @@ func TestExtractGetToContentUrlPositive(t *testing.T) { func TestExtractGetToContentUrlNegative(t *testing.T) { //given - samlResposeData, err := ioutil.ReadFile("responses/samlRespose.html") + samlResposeData, err := os.ReadFile("responses/samlRespose.html") require.Nil(t, err) //when @@ -86,7 +86,7 @@ func TestExtractGetToContentUrlNegative(t *testing.T) { func TestExtractGetToContentUrlDiv(t *testing.T) { //given - getToContentData, err := ioutil.ReadFile("responses/getToContentDiv.html") + getToContentData, err := os.ReadFile("responses/getToContentDiv.html") require.Nil(t, err) expectedResourceUrl := "/nidp/app/login?id=contract_kerb&sid=0&option=credential&sid=0" @@ -102,7 +102,7 @@ func TestExtractGetToContentUrlDiv(t *testing.T) { func TestExtractWinLocHrefUrlPositive(t *testing.T) { //given - winLocHrefData, err := ioutil.ReadFile("responses/winLocHref.html") + winLocHrefData, err := os.ReadFile("responses/winLocHref.html") require.Nil(t, err) expectedResourceUrl := "https://login.authbridge.somegroup.com/nidp/saml2/idpsend?PID=STSPv8a5kc" @@ -118,7 +118,7 @@ func TestExtractWinLocHrefUrlPositive(t *testing.T) { func TestExtractWinLocHrefUrlNegative(t *testing.T) { //given - samlResposeData, err := ioutil.ReadFile("responses/samlRespose.html") + samlResposeData, err := os.ReadFile("responses/samlRespose.html") require.Nil(t, err) //when @@ -133,7 +133,7 @@ func TestExtractWinLocHrefUrlNegative(t *testing.T) { func TestExtractIDPLoginPassPositive(t *testing.T) { //given - idpLoginPassData, err := ioutil.ReadFile("responses/idpLoginPass.html") + idpLoginPassData, err := os.ReadFile("responses/idpLoginPass.html") require.Nil(t, err) expectedForm := &page.Form{ URL: "https://login.authbridge.somegroup.com/nidp/app/login?sid=0&sid=0", @@ -153,7 +153,7 @@ func TestExtractIDPLoginPassPositive(t *testing.T) { func TestExtractIDPLoginPassNegative(t *testing.T) { //given - idpLoginRsaData, err := ioutil.ReadFile("responses/idpLoginRsa.html") + idpLoginRsaData, err := os.ReadFile("responses/idpLoginRsa.html") require.Nil(t, err) //when @@ -168,7 +168,7 @@ func TestExtractIDPLoginPassNegative(t *testing.T) { func TestExtractPrivilegedIDPLoginPassPositive(t *testing.T) { //given - idpLoginPassData, err := ioutil.ReadFile("responses/privileged_flow/idpLoginPass.html") + idpLoginPassData, err := os.ReadFile("responses/privileged_flow/idpLoginPass.html") require.Nil(t, err) expectedForm := &page.Form{ URL: "https://login.authbridge.somegroup.com/nidp/app/login?sid=0&sid=0", @@ -188,7 +188,7 @@ func TestExtractPrivilegedIDPLoginPassPositive(t *testing.T) { func TestExtractIDPLoginRsaPositive(t *testing.T) { //given - idpLoginRsaData, err := ioutil.ReadFile("responses/idpLoginRsa.html") + idpLoginRsaData, err := os.ReadFile("responses/idpLoginRsa.html") require.Nil(t, err) expectedForm := &page.Form{ URL: "https://login.authbridge.somegroup.com/nidp/app/login?sid=11&sid=11", @@ -208,7 +208,7 @@ func TestExtractIDPLoginRsaPositive(t *testing.T) { func TestExtractIDPLoginRsaNegative(t *testing.T) { //given - idpLoginPassData, err := ioutil.ReadFile("responses/idpLoginPass.html") + idpLoginPassData, err := os.ReadFile("responses/idpLoginPass.html") require.Nil(t, err) //when @@ -223,7 +223,7 @@ func TestExtractIDPLoginRsaNegative(t *testing.T) { func TestExtractPrivilegedIDPLoginRsaNegative(t *testing.T) { //given - idpLoginPassData, err := ioutil.ReadFile("responses/privileged_flow/idpLoginPass.html") + idpLoginPassData, err := os.ReadFile("responses/privileged_flow/idpLoginPass.html") require.Nil(t, err) //when diff --git a/pkg/provider/okta/okta.go b/pkg/provider/okta/okta.go index 7d8771782..52e9ba808 100644 --- a/pkg/provider/okta/okta.go +++ b/pkg/provider/okta/okta.go @@ -3,11 +3,12 @@ package okta import ( "bytes" "context" + "crypto/tls" "encoding/base64" "encoding/json" "fmt" "html" - "io/ioutil" + "io" "log" "net/http" "net/http/cookiejar" @@ -15,6 +16,7 @@ import ( "regexp" "strconv" "strings" + "sync" "time" "github.com/PuerkitoBio/goquery" @@ -180,7 +182,7 @@ func (oc *Client) createSession(loginDetails *creds.LoginDetails, sessionToken s return "", "", errors.Wrap(err, "error retrieving session response") } - body, err := ioutil.ReadAll(res.Body) + body, err := io.ReadAll(res.Body) if err != nil { return "", "", errors.Wrap(err, "error retrieving body from response") } @@ -257,7 +259,7 @@ func (oc *Client) validateSession(loginDetails *creds.LoginDetails) error { return errors.Wrap(err, "error retrieving session response") } - body, err := ioutil.ReadAll(res.Body) + body, err := io.ReadAll(res.Body) if err != nil { return errors.Wrap(err, "error retrieving body from response") } @@ -314,7 +316,7 @@ func (oc *Client) authWithSession(loginDetails *creds.LoginDetails) (string, err logger.Debugf("error authing with session: %v", err) } - body, err := ioutil.ReadAll(res.Body) + body, err := io.ReadAll(res.Body) if err != nil { logger.Debugf("error reading body for auth with session: %v", err) } @@ -426,7 +428,7 @@ func (oc *Client) primaryAuth(loginDetails *creds.LoginDetails) (string, string, return "", "", "", errors.Wrap(err, "error retrieving auth response") } - body, err := ioutil.ReadAll(res.Body) + body, err := io.ReadAll(res.Body) if err != nil { return "", "", "", errors.Wrap(err, "error retrieving body from response") } @@ -557,21 +559,9 @@ func (oc *Client) follow(ctx context.Context, req *http.Request, loginDetails *c logger.WithField("type", "saml-response").Debug("doc detect") handler = oc.handleFormRedirect } else { - req, err = http.NewRequest("GET", loginDetails.URL, nil) + stateToken, err := oc.getStateToken(req, loginDetails) if err != nil { - return "", errors.Wrap(err, "error building app request") - } - res, err = oc.client.Do(req) - if err != nil { - return "", errors.Wrap(err, "error retrieving app response") - } - body, err := ioutil.ReadAll(res.Body) - if err != nil { - return "", errors.Wrap(err, "error retrieving body from response") - } - stateToken, err := getStateTokenFromOktaPageBody(string(body)) - if err != nil { - return "", errors.Wrap(err, "error retrieving saml response") + return "", errors.Wrap(err, "failed to getStateToken") } loginDetails.StateToken = stateToken return oc.Authenticate(loginDetails) @@ -591,19 +581,58 @@ func (oc *Client) follow(ctx context.Context, req *http.Request, loginDetails *c } +func (oc *Client) getStateToken(req *http.Request, loginDetails *creds.LoginDetails) (string, error) { + url, err := url.Parse(loginDetails.URL) + if err != nil { + return "", errors.Wrap(err, "error building app request") + } + + req.URL = url + req.Method = "GET" + req.Body = nil + + res, err := oc.client.Do(req) + if err != nil { + return "", errors.Wrap(err, "error retrieving app response") + } + body, err := io.ReadAll(res.Body) + if err != nil { + return "", errors.Wrap(err, "error retrieving body from response") + } + stateToken, err := getStateTokenFromOktaPageBody(string(body)) + if err != nil { + return "", errors.Wrap(err, "error retrieving saml response") + } + return stateToken, nil +} + func getStateTokenFromOktaPageBody(responseBody string) (string, error) { - re := regexp.MustCompile("var stateToken = [\"|'](.*)[\"|'];") - match := re.FindStringSubmatch(responseBody) - if len(match) < 2 { - return "", errors.New("cannot find state token") + regexes := []*regexp.Regexp{ + regexp.MustCompile("var stateToken = [\"|'](.*)[\"|'];"), + // Found on the "extra verification" page + // hiding in a Javascript object + regexp.MustCompile(`"stateToken":"([^"]*)"`), + } + + for _, re := range regexes { + match := re.FindStringSubmatch(responseBody) + if len(match) >= 2 { + return strings.Replace(match[1], `\x2D`, "-", -1), nil + } } - return strings.Replace(match[1], `\x2D`, "-", -1), nil + + return "", errors.New("cannot find state token") + } -func parseMfaIdentifer(json string, arrayPosition int) string { +func parseMfaIdentifer(json string, arrayPosition int) (string, string, string) { mfaProvider := gjson.Get(json, fmt.Sprintf("_embedded.factors.%d.provider", arrayPosition)).String() factorType := strings.ToUpper(gjson.Get(json, fmt.Sprintf("_embedded.factors.%d.factorType", arrayPosition)).String()) - return fmt.Sprintf("%s %s", mfaProvider, factorType) + id := gjson.Get(json, fmt.Sprintf("_embedded.factors.%d.id", arrayPosition)).String() + // Okta gives names to some authentication methods + // displaying this name is useful when there's multiple auths of the same type. e.g. multiple FIDO options + authName := gjson.Get(json, fmt.Sprintf("_embedded.factors.%d.profile.authenticatorName", arrayPosition)).String() + return fmt.Sprintf("%s %s", mfaProvider, factorType), authName, id } func (oc *Client) handleFormRedirect(ctx context.Context, doc *goquery.Document) (context.Context, *http.Request, error) { @@ -667,7 +696,15 @@ func getMfaChallengeContext(oc *Client, mfaOption int, resp string) (*mfaChallen stateToken := gjson.Get(resp, "stateToken").String() factorID := gjson.Get(resp, fmt.Sprintf("_embedded.factors.%d.id", mfaOption)).String() oktaVerify := gjson.Get(resp, fmt.Sprintf("_embedded.factors.%d._links.verify.href", mfaOption)).String() - mfaIdentifer := parseMfaIdentifer(resp, mfaOption) + mfaIdentifer, _, _ := parseMfaIdentifer(resp, mfaOption) + + if !strings.Contains(oktaVerify, "rememberDevice") { + separator := "?" + if strings.Contains(oktaVerify, "?") { + separator = "&" + } + oktaVerify = oktaVerify + separator + "rememberDevice=" + strconv.FormatBool(oc.rememberDevice) + } logger.WithField("factorID", factorID).WithField("oktaVerify", oktaVerify).WithField("mfaIdentifer", mfaIdentifer).Debug("MFA") @@ -707,7 +744,7 @@ func getMfaChallengeContext(oc *Client, mfaOption int, resp string) (*mfaChallen return nil, errors.Wrap(err, "error retrieving verify response") } - body, err := ioutil.ReadAll(res.Body) + body, err := io.ReadAll(res.Body) if err != nil { return nil, errors.Wrap(err, "error retrieving body from response") } @@ -727,16 +764,41 @@ func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails, mfaOption := 0 var mfaOptions []string for i := range gjson.Get(resp, "_embedded.factors").Array() { - identifier := parseMfaIdentifer(resp, i) + identifier, authName, id := parseMfaIdentifer(resp, i) if val, ok := supportedMfaOptions[identifier]; ok { - mfaOptions = append(mfaOptions, val) + // If the authentication method as a name, we add it to the MFA option. + // This makes it possible to identify which method to choose + if len(authName) > 0 { + mfaOptions = append(mfaOptions, fmt.Sprintf("%s - %s (%s)", val, authName, id)) + } else { + mfaOptions = append(mfaOptions, fmt.Sprintf("%s - %s", val, id)) + } + } else { mfaOptions = append(mfaOptions, "UNSUPPORTED: "+identifier) } } if strings.ToUpper(oc.mfa) != "AUTO" { - mfaOption = findMfaOption(oc.mfa, mfaOptions, 0) + var mfaOptionsMatches []string + // Collect all options that match the chosen MFA + // It will be more than 1 when there's multiple MFA of the same type configured - e.g.: multiple FIDO methods + for _, option := range mfaOptions { + if strings.HasPrefix(strings.ToUpper(option), oc.mfa) { + mfaOptionsMatches = append(mfaOptionsMatches, option) + } + } + // If multiple MFA of the same type are found, we prompt the user to pick which one to use + if len(mfaOptionsMatches) > 1 { + matchOptionIndex := prompter.Choose(fmt.Sprintf("Multiple %s MFA options found. Select which MFA option to use", oc.mfa), mfaOptionsMatches) + for i := range mfaOptions { + if mfaOptions[i] == mfaOptionsMatches[matchOptionIndex] { + mfaOption = i + } + } + } else { + mfaOption = findMfaOption(oc.mfa, mfaOptions, 0) + } } else if len(mfaOptions) > 1 { mfaOption = prompter.Choose("Select which MFA option to use", mfaOptions) } @@ -774,14 +836,7 @@ func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails, return "", errors.Wrap(err, "error retrieving token post response") } - body, err := ioutil.ReadAll(res.Body) - if err != nil { - return "", errors.Wrap(err, "error retrieving body from response") - } - - resp = string(body) - - return gjson.Get(resp, "sessionToken").String(), nil + return extractSessionToken(res.Body) case IdentifierPushMfa: @@ -828,7 +883,7 @@ func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails, case IdentifierDuoMfa: duoHost := gjson.Get(challengeContext.challengeResponseBody, "_embedded.factor._embedded.verification.host").String() duoSignature := gjson.Get(challengeContext.challengeResponseBody, "_embedded.factor._embedded.verification.signature").String() - duoSiguatres := strings.Split(duoSignature, ":") + duoSignatures := strings.Split(duoSignature, ":") //duoSignatures[0] = TX //duoSignatures[1] = APP duoCallback := gjson.Get(challengeContext.challengeResponseBody, "_embedded.factor._embedded.verification._links.complete.href").String() @@ -839,18 +894,27 @@ func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails, duoForm := url.Values{} duoForm.Add("parent", fmt.Sprintf("https://%s/signin/verify/duo/web", oktaOrgHost)) duoForm.Add("java_version", "") - duoForm.Add("java_version", "") duoForm.Add("flash_version", "") duoForm.Add("screen_resolution_width", "3008") duoForm.Add("screen_resolution_height", "1692") duoForm.Add("color_depth", "24") + duoForm.Add("is_cef_browser", "false") + duoForm.Add("is_ipad_os", "false") + duoForm.Add("is_ie_compatability_mode", "") + duoForm.Add("acting_ie_version", "") + duoForm.Add("react_support", "true") + duoForm.Add("react_support_error_message", "") + duoForm.Add("tx", duoSignatures[0]) req, err := http.NewRequest("POST", duoSubmitURL, strings.NewReader(duoForm.Encode())) if err != nil { return "", errors.Wrap(err, "error building authentication request") } + q := req.URL.Query() - q.Add("tx", duoSiguatres[0]) + q.Add("tx", duoSignatures[0]) + q.Add("parent", fmt.Sprintf("https://%s/signin/verify/duo/web", oktaOrgHost)) + q.Add("v", "2.8") req.URL.RawQuery = q.Encode() req.Header.Add("Content-Type", "application/x-www-form-urlencoded") @@ -860,33 +924,61 @@ func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails, return "", errors.Wrap(err, "error retrieving verify response") } - //try to extract sid + // At this point, if device trust is enabled we need to go on that tangent doc, err := goquery.NewDocumentFromReader(res.Body) + if err != nil { return "", errors.Wrap(err, "error parsing document") } + if doc.Find("form[id=\"client_cert_form\"]").Length() > 0 { + doc, err = verifyTrustedCert(oc, doc, duoHost, duoSubmitURL, q) + if err != nil { + return "", errors.Wrap(err, "couldn't validate client cert") + } + + } else if doc.Find("form[id=\"endpoint-health-form\"]").Length() > 0 { + origUrl := req.URL.String() + duoEndpointHost := "127.0.0.1:53100" + doc, err = verifyEndpointHealth(oc, doc, origUrl, duoEndpointHost, duoHost, duoSubmitURL, q) + + if err != nil { + return "", errors.Wrap(err, "couldn't validate endpoint health") + + } + + } + duoSID, ok := doc.Find("input[name=\"sid\"]").Attr("value") if !ok { return "", errors.Wrap(err, "unable to locate saml response") } duoSID = html.UnescapeString(duoSID) - //prompt for mfa type - //only supporting push or passcode for now + var duoMfaOptions = []string{} var token string - var duoMfaOptions = []string{ - "Duo Push", - "Passcode", + webauthnOption := doc.Find("option[name=\"webauthn\"]") + + if webauthnOption.Length() > 0 { + token, _ = webauthnOption.Attr("value") + duoMfaOptions = append(duoMfaOptions, "U2F Key") + } + + if doc.Find("option[value=\"phone1\"]").Length() > 0 { + duoMfaOptions = append(duoMfaOptions, "Duo Push") + } + + if doc.Find("option[value=\"token\"]").Length() > 0 { + duoMfaOptions = append(duoMfaOptions, "Passcode") } duoMfaOption := 0 if loginDetails.DuoMFAOption == "Duo Push" { - duoMfaOption = 0 - } else if loginDetails.DuoMFAOption == "Passcode" { duoMfaOption = 1 + } else if loginDetails.DuoMFAOption == "Passcode" { + duoMfaOption = 2 } else { duoMfaOption = prompter.Choose("Select a DUO MFA Option", duoMfaOptions) } @@ -901,11 +993,19 @@ func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails, duoForm = url.Values{} duoForm.Add("sid", duoSID) - duoForm.Add("device", "phone1") - duoForm.Add("factor", duoMfaOptions[duoMfaOption]) duoForm.Add("out_of_date", "false") - if duoMfaOptions[duoMfaOption] == "Passcode" { + + switch duoMfaOptions[duoMfaOption] { + case "Passcode": duoForm.Add("passcode", token) + duoForm.Add("device", "phone1") + duoForm.Add("factor", "Passcode") + case "Duo Push": + duoForm.Add("device", "phone1") + duoForm.Add("factor", "Duo Push") + case "U2F Key": + duoForm.Add("device", "u2f_token") + duoForm.Add("factor", "U2F Token") } req, err = http.NewRequest("POST", duoSubmitURL, strings.NewReader(duoForm.Encode())) @@ -920,7 +1020,7 @@ func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails, return "", errors.Wrap(err, "error retrieving verify response") } - body, err := ioutil.ReadAll(res.Body) + body, err := io.ReadAll(res.Body) if err != nil { return "", errors.Wrap(err, "error retrieving body from response") } @@ -952,7 +1052,7 @@ func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails, return "", errors.Wrap(err, "error retrieving verify response") } - body, err = ioutil.ReadAll(res.Body) + body, err = io.ReadAll(res.Body) if err != nil { return "", errors.Wrap(err, "error retrieving body from response") } @@ -966,7 +1066,105 @@ func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails, duoSID = newSID } - log.Println(gjson.Get(resp, "response.status").String()) + // Do the webauthn + duoRestStatusCode := gjson.Get(resp, "response.status_code").String() + + if duoRestStatusCode == "u2f_sent" { + appId := gjson.Get(resp, "response.u2f_sign_request.0.appId").String() + //appId := "api-23a9854b.duosecurity.com" + version := gjson.Get(resp, "response.u2f_sign_request.0.version").String() + challengeNonce := gjson.Get(resp, "response.u2f_sign_request.0.challenge").String() + keyHandle := gjson.Get(resp, "response.u2f_sign_request.0.keyHandle").String() + sessionId := gjson.Get(resp, "response.u2f_sign_request.0.sessionId").String() + + u2fClient, err := NewDUOU2FClient(challengeNonce, appId, version, keyHandle, sessionId, new(U2FDeviceFinder)) + + if err != nil { + return "", err + } + + rd, err := u2fClient.ChallengeU2F() + if err != nil { + return "", err + } + + payload, err := json.Marshal(rd) + if err != nil { + return "", err + } + + duoForm = url.Values{} + duoForm.Add("sid", duoSID) + duoForm.Add("device", "u2f_token") + duoForm.Add("factor", "u2f_finish") + duoForm.Add("days_to_block", "None") + duoForm.Add("out_of_date", "False") + duoForm.Add("days_out_of_date", "0") + duoForm.Add("response_data", string(payload)) + + duoSubmitURL = fmt.Sprintf("https://%s/frame/prompt", duoHost) + + req, err = http.NewRequest("POST", duoSubmitURL, strings.NewReader(duoForm.Encode())) + if err != nil { + return "", errors.Wrap(err, "error building authentication request") + } + + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + res, err = oc.client.Do(req) + if err != nil { + return "", errors.Wrap(err, "error retrieving verify response") + } + + body, err = io.ReadAll(res.Body) + if err != nil { + return "", errors.Wrap(err, "error retrieving body from response") + } + + resp = string(body) + + duoTxStat := gjson.Get(resp, "stat").String() + duoTxID := gjson.Get(resp, "response.txid").String() + if duoTxStat != "OK" { + return "", errors.New("error authenticating mfa device") + } + + // get duo cookie + duoSubmitURL = fmt.Sprintf("https://%s/frame/status", duoHost) + + duoForm = url.Values{} + duoForm.Add("sid", duoSID) + duoForm.Add("txid", duoTxID) + + req, err = http.NewRequest("POST", duoSubmitURL, strings.NewReader(duoForm.Encode())) + if err != nil { + return "", errors.Wrap(err, "error building authentication request") + } + + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + res, err = oc.client.Do(req) + if err != nil { + return "", errors.Wrap(err, "error retrieving verify response") + } + + defer res.Body.Close() + + body, err = io.ReadAll(res.Body) + if err != nil { + return "", errors.Wrap(err, "error retrieving body from response") + } + + resp = string(body) + + duoTxResult = gjson.Get(resp, "response.result").String() + duoResultURL = gjson.Get(resp, "response.result_url").String() + newSID = gjson.Get(resp, "response.sid").String() + if newSID != "" { + duoSID = newSID + } + + } if duoTxResult != "SUCCESS" { //poll as this is likely a push request @@ -985,7 +1183,7 @@ func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails, return "", errors.Wrap(err, "error retrieving verify response") } - body, err = ioutil.ReadAll(res.Body) + body, err = io.ReadAll(res.Body) if err != nil { return "", errors.Wrap(err, "error retrieving body from response") } @@ -1028,12 +1226,12 @@ func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails, return "", errors.Wrap(err, "error retrieving duo result response") } - body, err = ioutil.ReadAll(res.Body) + body, err = io.ReadAll(res.Body) if err != nil { return "", errors.Wrap(err, "duoResultSubmit: error retrieving body from response") } - resp := string(body) + resp = string(body) duoTxStat = gjson.Get(resp, "stat").String() if duoTxStat != "OK" { @@ -1050,7 +1248,7 @@ func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails, oktaForm := url.Values{} oktaForm.Add("id", challengeContext.factorID) oktaForm.Add("stateToken", stateToken) - oktaForm.Add("sig_response", fmt.Sprintf("%s:%s", duoTxCookie, duoSiguatres[1])) + oktaForm.Add("sig_response", fmt.Sprintf("%s:%s", duoTxCookie, duoSignatures[1])) req, err = http.NewRequest("POST", duoCallback, strings.NewReader(oktaForm.Encode())) if err != nil { @@ -1087,7 +1285,7 @@ func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails, return "", errors.Wrap(err, "error retrieving verify response") } - body, err = ioutil.ReadAll(res.Body) + body, err = io.ReadAll(res.Body) if err != nil { return "", errors.Wrap(err, "error retrieving body from response") } @@ -1102,6 +1300,25 @@ func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails, return "", errors.New("no mfa options provided") } +func extractSessionToken(r io.Reader) (string, error) { + bb, err := io.ReadAll(r) + if err != nil { + return "", errors.Wrap(err, "error retrieving body from response") + } + + resp := string(bb) + sessionToken := gjson.Get(resp, "sessionToken").String() + if sessionToken == "" { + status := gjson.Get(resp, "status").String() + if status != "" { + return "", errors.Errorf("response does not contain session token, received status is: %q", status) + } + return "", errors.Errorf("response does not contain session token") + } + + return gjson.Get(resp, "sessionToken").String(), nil +} + func fidoWebAuthn(oc *Client, oktaOrgHost string, challengeContext *mfaChallengeContext, mfaOption int, stateToken string, mfaOptions []string, resp string) (string, error) { var signedAssertion *SignedAssertion @@ -1168,10 +1385,221 @@ func fidoWebAuthn(oc *Client, oktaOrgHost string, challengeContext *mfaChallenge return "", errors.Wrap(err, "error retrieving verify response") } - body, err := ioutil.ReadAll(res.Body) + body, err := io.ReadAll(res.Body) if err != nil { return "", errors.Wrap(err, "error retrieving body from response") } return gjson.GetBytes(body, "sessionToken").String(), nil } + +func verifyTrustedCert(oc *Client, doc *goquery.Document, duoHost string, duoSubmitURL string, q url.Values) (*goquery.Document, error) { + // If you enable DUO trusted cert validation, it requires an extra step before continuing. + // The way the validation process works is it attempts to send a request to a localhost:15310 + // where the DUO cert proxy may be running. This returns a JSON blob if it was successful. + // If that isn't running, there is also a public DUO endpoint you can use for the validation. + // This code attempts to hit the local validator, then the remote one if it is not available, + // which is the same flow the webpage does if you validate through a browser. + + // We then follow up again with a POST request to /frame/web/v1/auth, this time with the + // cert validation parameters in the POST body. If that succeeds, then we can continue + // along the existing request path. + + sid, _ := doc.Find("input[name=\"sid\"]").Attr("value") + certUrl, _ := doc.Find("input[name=\"certs_url\"]").Attr("value") + txid, _ := doc.Find("input[name=\"certs_txid\"]").Attr("value") + certifierUrl, _ := doc.Find("input[name=\"certifier_url\"]").Attr("value") + + duoUrl := fmt.Sprintf("%s?type=AJAX&sid=%s&certs_txid=%s", certUrl, url.QueryEscape(sid), txid) + duoCertifierURL := fmt.Sprintf("%s?certUrl=%s", certifierUrl, url.QueryEscape(duoUrl)) + + // The locally running certifier does not have a valid certificate, so we have to skip verification + customTransport := http.DefaultTransport.(*http.Transport).Clone() + customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + originalTransport := oc.client.Transport + + oc.client.Transport = customTransport + + req, err := http.NewRequest("GET", duoCertifierURL, nil) + + if err != nil { + return nil, errors.Wrap(err, "error building cert validation request") + } + + req.Header.Add("Referer", "https://"+duoHost+"/") + res, err := oc.client.Do(req) + oc.client.Transport = originalTransport + + if err != nil { + // Local certifier not running, try online one + duoCertURL := fmt.Sprintf("%s?sid=%s&certs_txid=%s&type=AJAX", certUrl, url.QueryEscape(sid), txid) + + req, err = http.NewRequest("GET", duoCertURL, nil) + + if err != nil { + return nil, errors.Wrap(err, "error building cert validation request ") + } + + req.Header.Add("Referer", "https://"+duoHost) + res, err = oc.client.Do(req) + + if err != nil { + return nil, errors.Wrap(err, "error retrieving cert validation response") + } + } + + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + return nil, errors.Wrap(err, "error retrieving body from response") + } + + resp := string(body) + + duoStat := gjson.Get(resp, "stat").String() + if duoStat != "OK" { + return nil, errors.New("error validation certificate") + } + + certForm := url.Values{} + certForm.Add("sid", sid) + certForm.Add("certs_url", certUrl) + certForm.Add("certs_txid", txid) + certForm.Add("certifier_url", certifierUrl) + + // Try POST again + req, err = http.NewRequest("POST", duoSubmitURL, strings.NewReader(certForm.Encode())) + if err != nil { + return nil, errors.Wrap(err, "error building authentication request") + } + + req.URL.RawQuery = q.Encode() + + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + res, err = oc.client.Do(req) + if err != nil { + return nil, errors.Wrap(err, "error retrieving verify response") + } + defer res.Body.Close() + + doc, err = goquery.NewDocumentFromReader(res.Body) + if err != nil { + return nil, errors.Wrap(err, "error parsing document") + } + + return doc, nil +} + +func verifyEndpointHealth(oc *Client, doc *goquery.Document, origURL string, duoEndpointHost string, duoHost string, duoSubmitURL string, postQuery url.Values) (*goquery.Document, error) { + + txid, _ := doc.Find("input[name=\"txid\"]").Attr("value") + sid, _ := doc.Find("input[name=\"sid\"]").Attr("value") + ehServiceUrl, _ := doc.Find("input[name=\"eh_service_url\"]").Attr("value") + akey, _ := doc.Find("input[name=\"akey\"]").Attr("value") + responseTimeout, _ := doc.Find("input[name=\"response_timeout\"]").Attr("value") + parent, _ := doc.Find("input[name=\"parent\"]").Attr("value") + duoAppUrl, _ := doc.Find("input[name=\"duo_app_url\"]").Attr("value") + ehDownloadLink, _ := doc.Find("input[name=\"eh_download_link\"]").Attr("value") + // isSilentCollection, _ := doc.Find("input[name=\"is_silent_collection\"]").Attr("value") + + timestamp := strconv.Itoa((int)(time.Now().Unix())) + duoAliveUrl := fmt.Sprintf("https://%s/alive", duoEndpointHost) + req, _ := http.NewRequest("GET", duoAliveUrl, nil) + + q := req.URL.Query() + q.Add("_", timestamp+"100") + + req.URL.RawQuery = q.Encode() + req.Header.Add("Referer", "https://"+duoHost+"/") + req.Header.Add("Origin", "https://"+duoHost) + + _, err := oc.client.Do(req) + if err != nil { + return nil, errors.Wrapf(err, "failed to query alive URL %s", duoAliveUrl) + + } + + duoCheckEndpointAppURL := fmt.Sprintf("https://%s/frame/check_endpoint_app_status", duoHost) + req, _ = http.NewRequest("GET", duoCheckEndpointAppURL, nil) + + q = req.URL.Query() + + q.Add("txid", txid) + q.Add("sid", sid) + + req.URL.RawQuery = q.Encode() + + req.Header.Add("Referer", origURL) + req.Header.Add("X-Requested-With", "XMLHttpRequest") + + var wg sync.WaitGroup + wg.Add(1) + + // check_endpoint_app_status blocks until the healthurl report is queried, so we need to use goroutines. + go func(r *http.Request) { + res, _ := oc.client.Do(req) + defer res.Body.Close() + wg.Done() + }(req) + + // Separator + + duoHealthURL := fmt.Sprintf("https://%s/report", duoEndpointHost) + + req2, _ := http.NewRequest("GET", duoHealthURL, nil) + + q = req2.URL.Query() + + q.Add("txid", txid) + q.Add("eh_service_url", ehServiceUrl+"?_="+timestamp+"101") + + req2.URL.RawQuery = q.Encode() + req2.Header.Add("Referer", "https://"+duoHost+"/") + req2.Header.Add("Origin", "https://"+duoHost) + + res, err := oc.client.Do(req2) + if err == nil { + defer res.Body.Close() + } + + // Wait for check_endpoint_app_status to block + wg.Wait() + + // Try the call to /v1/frame/auth again + + certForm := url.Values{} + certForm.Add("sid", sid) + certForm.Add("txid", txid) + certForm.Add("eh_service_url", ehServiceUrl) + certForm.Add("akey", akey) + certForm.Add("response_timeout", responseTimeout) + certForm.Add("parent", parent) + certForm.Add("duo_app_url", duoAppUrl) + certForm.Add("eh_download_link", ehDownloadLink) + // certForm.Add("is_silent_collection", isSilentCollection) + + time.Sleep(2 * time.Second) + + // Try POST again + req, err = http.NewRequest("POST", duoSubmitURL, strings.NewReader(certForm.Encode())) + if err != nil { + return nil, errors.Wrap(err, "error building authentication request") + } + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + req.URL.RawQuery = postQuery.Encode() + + res, err = oc.client.Do(req) + if err != nil { + return nil, errors.Wrap(err, "error retrieving verify response") + } + defer res.Body.Close() + + doc, err = goquery.NewDocumentFromReader(res.Body) + if err != nil { + return nil, errors.Wrap(err, "error parsing document") + } + return doc, nil +} diff --git a/pkg/provider/okta/okta_duo_u2f.go b/pkg/provider/okta/okta_duo_u2f.go new file mode 100644 index 000000000..db682bb64 --- /dev/null +++ b/pkg/provider/okta/okta_duo_u2f.go @@ -0,0 +1,110 @@ +package okta + +import ( + "errors" + "fmt" + "time" + + "github.com/marshallbrekka/go-u2fhost" +) + +// DUOU2fClient represents a challenge and the device used to respond +type DUOU2FClient struct { + ChallengeNonce string + AppID string + Version string + Device u2fhost.Device + KeyHandle string + StateToken string +} + +// ResponseData is passed back to DUO as a response +type ResponseData struct { + SessionId string `json:"sessionId"` + ClientData string `json:"clientData"` + SignatureData string `json:"signatureData"` + KeyHandle string `json:"keyHandle"` +} + +// NewFidoClient returns a new initialized FIDO1-based WebAuthnClient, representing a single device +func NewDUOU2FClient(challengeNonce, appID, version, keyHandle, stateToken string, deviceFinder DeviceFinder) (*DUOU2FClient, error) { + var device u2fhost.Device + var err error + + retryCount := 0 + for retryCount < MaxOpenRetries { + device, err = deviceFinder.findDevice() + if err != nil { + if err == errNoDeviceFound { + return nil, err + } + + retryCount++ + time.Sleep(RetryDelayMS) + continue + } + + return &DUOU2FClient{ + Device: device, + ChallengeNonce: challengeNonce, + AppID: appID, + Version: version, + KeyHandle: keyHandle, + StateToken: stateToken, + }, nil + } + + return nil, fmt.Errorf("failed to create client: %s. exceeded max retries of %d", err, MaxOpenRetries) +} + +// ChallengeU2F takes a FidoClient and returns a signed assertion to send to Okta +func (d *DUOU2FClient) ChallengeU2F() (*ResponseData, error) { + if d.Device == nil { + return nil, errors.New("No Device Found") + } + request := &u2fhost.AuthenticateRequest{ + Challenge: d.ChallengeNonce, + Facet: d.AppID, + AppId: d.AppID, + KeyHandle: d.KeyHandle, + WebAuthn: false, + } + // do the change + prompted := false + timeout := time.After(time.Second * 25) + interval := time.NewTicker(time.Millisecond * 250) + var responsePayload *ResponseData + + defer func() { + d.Device.Close() + }() + defer interval.Stop() + for { + select { + case <-timeout: + return nil, errors.New("Failed to get authentication response after 25 seconds") + case <-interval.C: + response, err := d.Device.Authenticate(request) + if err == nil { + responsePayload = &ResponseData{ + SessionId: d.StateToken, + ClientData: response.ClientData, + SignatureData: response.SignatureData, + KeyHandle: d.KeyHandle, + } + fmt.Printf(" ==> Touch accepted. Proceeding with authentication\n") + return responsePayload, nil + } + + switch err.(type) { + case *u2fhost.TestOfUserPresenceRequiredError: + if !prompted { + fmt.Printf("\nTouch the flashing U2F device to authenticate...\n") + prompted = true + } + default: + return responsePayload, err + } + } + } +} diff --git a/pkg/provider/okta/okta_duo_u2f_test.go b/pkg/provider/okta/okta_duo_u2f_test.go new file mode 100644 index 000000000..b7fb7591e --- /dev/null +++ b/pkg/provider/okta/okta_duo_u2f_test.go @@ -0,0 +1,69 @@ +package okta + +import ( + "testing" + + "github.com/marshallbrekka/go-u2fhost" + "github.com/stretchr/testify/assert" + "github.com/versent/saml2aws/v2/mocks" +) + +func TestChallengeDuoU2F(t *testing.T) { + challengeNonce := "challengeNonce" + appID := "appID" + version := "version" + keyHandle := "keyHandle" + stateToken := "stateToken" + + request := &u2fhost.AuthenticateRequest{ + Challenge: challengeNonce, + Facet: appID, + AppId: appID, + KeyHandle: keyHandle, + WebAuthn: false, + } + + clientData := "exampleClientDat" + signatureData := "exampleSignatureData" + + response := &u2fhost.AuthenticateResponse{ + ClientData: clientData, + SignatureData: signatureData, + } + + device := &mocks.U2FDevice{} + mockDeviceFinder := &MockDeviceFinder{device} + device.On("Open").Return(nil) + device.On("Close").Return(nil) + + client, err := NewDUOU2FClient(challengeNonce, appID, version, keyHandle, stateToken, mockDeviceFinder) + assert.NoError(t, err) + + t.Run("error", func(t *testing.T) { + device.On("Authenticate", request).Return(nil, &u2fhost.BadKeyHandleError{}).Once() + + resp, err := client.ChallengeU2F() + assert.Nil(t, resp) + assert.ErrorIs(t, err, &u2fhost.BadKeyHandleError{}) + }) + + t.Run("retry", func(t *testing.T) { + device.On("Authenticate", request).Return(nil, &u2fhost.TestOfUserPresenceRequiredError{}).Once() + device.On("Authenticate", request).Return(response, nil).Once() + + resp, err := client.ChallengeU2F() + assert.NoError(t, err) + assert.NotNil(t, resp) + }) + + t.Run("success", func(t *testing.T) { + device.On("Authenticate", request).Return(response, nil).Once() + + resp, err := client.ChallengeU2F() + assert.NoError(t, err) + assert.Equal(t, stateToken, resp.SessionId) + assert.Equal(t, clientData, resp.ClientData) + assert.Equal(t, signatureData, resp.SignatureData) + assert.Equal(t, keyHandle, resp.KeyHandle) + }) +} diff --git a/pkg/provider/okta/okta_test.go b/pkg/provider/okta/okta_test.go index 9bae1e4e9..e2d8f1fa4 100644 --- a/pkg/provider/okta/okta_test.go +++ b/pkg/provider/okta/okta_test.go @@ -1,14 +1,22 @@ package okta import ( + "crypto/tls" "errors" "fmt" + "io" + "net/http" + "net/http/httptest" "net/url" + "strings" "testing" + "testing/iotest" + "github.com/PuerkitoBio/goquery" "github.com/stretchr/testify/assert" "github.com/versent/saml2aws/v2/pkg/cfg" "github.com/versent/saml2aws/v2/pkg/creds" + "github.com/versent/saml2aws/v2/pkg/provider" ) type stateTokenTests struct { @@ -18,6 +26,13 @@ type stateTokenTests struct { err error } +type parseMfaIdentifierTests struct { + title string + identifier string + authName string + index int +} + func TestGetStateTokenFromOktaPageBody(t *testing.T) { tests := []stateTokenTests{ { @@ -27,17 +42,23 @@ func TestGetStateTokenFromOktaPageBody(t *testing.T) { err: nil, }, { - title: "State token not in body casues error", + title: "State token not in body causes error", body: "someJavascriptCode();\nsomeOtherJavaScriptCode();", stateToken: "", err: errors.New("cannot find state token"), }, { - title: "State token with hypen handled correctly", + title: "State token with hyphen handled correctly", body: "someJavascriptCode();\nvar stateToken = '12345\x2D6789';\nsomeOtherJavaScriptCode();", stateToken: "12345-6789", err: nil, }, + { + title: "javascript state token inside JSON", + body: `U0h8","stateToken":"c0ffeeda7e","helpLinks":{"help"`, + stateToken: "c0ffeeda7e", + err: nil, + }, } for _, test := range tests { t.Run(test.title, func(t *testing.T) { @@ -52,6 +73,140 @@ func TestGetStateTokenFromOktaPageBody(t *testing.T) { } } +func TestExtractSessionToken(t *testing.T) { + tests := []struct { + name string + r io.Reader + expectedToken string + expectedError string + }{ + { + name: "response with session token", + r: strings.NewReader(`{"sessionToken": "xxxx"}`), + expectedToken: "xxxx", + }, + { + name: "response with no session token but with status", + r: strings.NewReader(`{"status": "invalid password"}`), + expectedError: "response does not contain session token, received status is: \"invalid password\"", + }, + { + name: "response with no session token and no status", + r: strings.NewReader(`{}`), + expectedError: "response does not contain session token", + }, + { + name: "response is not even json", + r: strings.NewReader(`const x = {}`), + expectedError: "response does not contain session token", + }, + { + name: "reader returns an error", + r: iotest.ErrReader(fmt.Errorf("failed to read")), + expectedError: "error retrieving body from response: failed to read", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + resp, err := extractSessionToken(tc.r) + if tc.expectedError != "" { + if err == nil { + t.Fatalf("Expected error, but got null") + } + if err.Error() != tc.expectedError { + t.Fatalf("Expected error %q, but got %q", + err.Error(), tc.expectedError, + ) + } + } + if tc.expectedToken != "" { + if err != nil { + t.Fatalf("Expected token %q, but got error %v", tc.expectedToken, err) + } + if resp != tc.expectedToken { + t.Fatalf("Expected token %q, but got %q", tc.expectedToken, resp) + } + } + }) + } +} + +func TestGetMfaChallengeContext(t *testing.T) { + ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("OK")) + })) + defer ts.Close() + + t.Run("Verify link without query parameters", func(t *testing.T) { + oc, loginDetails := setupTestClient(t, ts, "PUSH") + + err := oc.setDeviceTokenCookie(loginDetails) + assert.Nil(t, err) + + context, err := getMfaChallengeContext(oc, 0, fmt.Sprintf(`{ + "stateToken": "TOKEN", + "_embedded": { + "factors": [ + { + "id": "PUSH", + "provider": "OKTA", + "factorType": "PUSH", + "_links": { + "verify": { "href": "%s/verify" } + } + } + ] + } + }`, ts.URL)) + assert.Nil(t, err) + + assert.Equal(t, ts.URL+"/verify?rememberDevice=true", context.oktaVerify) + }) + + t.Run("Verify link with query parameters", func(t *testing.T) { + oc, loginDetails := setupTestClient(t, ts, "PUSH") + + err := oc.setDeviceTokenCookie(loginDetails) + assert.Nil(t, err) + + context, err := getMfaChallengeContext(oc, 0, fmt.Sprintf(`{ + "stateToken": "TOKEN", + "_embedded": { + "factors": [ + { + "id": "PUSH", + "provider": "OKTA", + "factorType": "PUSH", + "_links": { + "verify": { "href": "%s/verify?p=1" } + } + } + ] + } + }`, ts.URL)) + assert.Nil(t, err) + + assert.Equal(t, ts.URL+"/verify?p=1&rememberDevice=true", context.oktaVerify) + }) +} + +func setupTestClient(t *testing.T, ts *httptest.Server, mfa string) (*Client, *creds.LoginDetails) { + testTransport := http.DefaultTransport.(*http.Transport).Clone() + testTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + opts := &provider.HTTPClientOptions{IsWithRetries: false} + client, _ := provider.NewHTTPClient(testTransport, opts) + ac := &Client{ + client: client, + targetURL: ts.URL, + mfa: mfa, + disableSessions: false, + rememberDevice: true, + } + loginDetails := &creds.LoginDetails{URL: ts.URL, Username: "user@example.com", Password: "test123"} + return ac, loginDetails +} + func TestSetDeviceTokenCookie(t *testing.T) { idpAccount := cfg.NewIDPAccount() idpAccount.URL = "https://idp.example.com/abcd" @@ -114,3 +269,284 @@ func TestOktaCfgFlagsCustomState(t *testing.T) { assert.False(t, oc.rememberDevice, fmt.Errorf("DisablDisableSessionseRememberDevice was set to true, so rememberDevice should be false")) } + +func TestOktaParseMfaIdentifer(t *testing.T) { + resp := `{ + "_embedded": { + "factors": [ + { + "factorType": "token:software:totp", + "provider": "GOOGLE", + "profile": { + "credentialId": "dade.murphy@example.com" + } + }, + { + "factorType":"webauthn", + "provider":"FIDO", + "profile":{ + "authenticatorName":"MacBook Touch ID" + } + }, + { + "factorType":"webauthn", + "provider":"FIDO", + "profile":{ + "authenticatorName":"Yubikey 5" + } + } + ] + } + }` + + tests := []parseMfaIdentifierTests{ + { + title: "Google TOTP doesn't have a name", + identifier: "GOOGLE TOKEN:SOFTWARE:TOTP", + authName: "", + index: 0, + }, + { + title: "WebAuthn tokens have names", + identifier: "FIDO WEBAUTHN", + authName: "MacBook Touch ID", + index: 1, + }, + { + title: "A second webauthn token with a different name", + identifier: "FIDO WEBAUTHN", + authName: "Yubikey 5", + index: 2, + }, + } + + for _, test := range tests { + t.Run(test.title, func(t *testing.T) { + identifier, authName, _ := parseMfaIdentifer(resp, test.index) + assert.Equal(t, test.identifier, identifier) + assert.Equal(t, test.authName, authName) + }) + } +} + +func TestGetStateToken(t *testing.T) { + + persistedCookie := &http.Cookie{Name: "TestCookie", Value: "test"} + svr := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Contains(t, r.Cookies(), persistedCookie) + + expected := "var stateToken = \"token1\";" + _, err := w.Write([]byte(expected)) + assert.Nil(t, err) + })) + defer svr.Close() + + idpAccount := cfg.NewIDPAccount() + idpAccount.URL = svr.URL + idpAccount.Username = "user@example.com" + idpAccount.SkipVerify = true + + loginDetails := &creds.LoginDetails{ + Username: idpAccount.Username, + Password: "abc123", + URL: idpAccount.URL, + } + + oc, err := New(idpAccount) + assert.Nil(t, err) + + req, _ := http.NewRequest("GET", "/", nil) + req.AddCookie(persistedCookie) + + stateToken, err := oc.getStateToken(req, loginDetails) + assert.Nil(t, err) + assert.Equal(t, "token1", stateToken) +} + +// anonymised from actual endpoint +const fakeEndpointForm = ` +
+ + + + + + + + + + + + + +
+` + +type testServer struct { + handlers []http.HandlerFunc + t *testing.T +} + +func (s *testServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var h http.HandlerFunc + if len(s.handlers) == 0 { + s.t.Errorf("unexpected request: %v", r.URL.String()) + w.WriteHeader(http.StatusInternalServerError) + return + } + h, s.handlers = s.handlers[0], s.handlers[1:] + h(w, r) +} +func TestVerifyTrustedCert(t *testing.T) { + host := "" + + handlers := []http.HandlerFunc{ + func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/certifier-url", r.URL.Path) + assert.Equal(t, fmt.Sprintf("https://%s/", host), r.Header.Get("Referer")) + + certURLRaw := r.URL.Query().Get("certUrl") + assert.NotEmpty(t, certURLRaw) + certURL, err := url.Parse(certURLRaw) + assert.NoError(t, err) + + assert.Equal(t, "AJAX", certURL.Query().Get("type")) + assert.Equal(t, "HAlorymR7ZuV2CxZz9T10jABE4jzkWFBJYLEnlF4nUwCqQ=|1683218343|2a46eb852b3ef9f662304cee03d008daed6d71f6", certURL.Query().Get("sid")) + + assert.Equal(t, "0dcfcbe4-5e20-47a3-9037-cb1d1bf4ad5b", certURL.Query().Get("certs_txid")) + + _, err = w.Write([]byte(`{"stat":"OK"}`)) + assert.NoError(t, err) + }, + + func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/submit", r.URL.Path) + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "1234567890", r.URL.Query().Get("tx")) + assert.Equal(t, "2.8", r.URL.Query().Get("v")) + + assert.NoError(t, r.ParseForm()) + + assert.Equal(t, "HAlorymR7ZuV2CxZz9T10jABE4jzkWFBJYLEnlF4nUwCqQ=|1683218343|2a46eb852b3ef9f662304cee03d008daed6d71f6", r.Form.Get("sid")) + assert.Equal(t, "0dcfcbe4-5e20-47a3-9037-cb1d1bf4ad5b", r.Form.Get("certs_txid")) + assert.Equal(t, fmt.Sprintf("https://%s/certs-url", host), r.Form.Get("certs_url")) + assert.Equal(t, fmt.Sprintf("https://%s/certifier-url", host), r.Form.Get("certifier_url")) + + }, + } + + mockServer := &testServer{handlers: handlers, t: t} + + ts := httptest.NewTLSServer(mockServer) + defer ts.Close() + + oc, _ := setupTestClient(t, ts, "PUSH") + upstreamURL, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("couldn't parse URL: %v", err) + } + + host = upstreamURL.Host + submitURL := ts.URL + "/submit" + + q := url.Values{} + q.Add("tx", "1234567890") + q.Add("parent", fmt.Sprintf("https://%s/signin/verify/duo/web", host)) + q.Add("v", "2.8") + + fakeForm := fmt.Sprintf(` +
+ + + + + + +
+`, host, host) + + doc, err := goquery.NewDocumentFromReader(strings.NewReader(fakeForm)) + if err != nil { + t.Fatalf("failed to validate document: %v, ", err) + } + + _, err = verifyTrustedCert(oc, doc, host, submitURL, q) + assert.NoError(t, err) + assert.Empty(t, mockServer.handlers) +} + +func TestVerifyEndpointHealth(t *testing.T) { + duoTX := "1234567890" + host := "" + + handlers := []http.HandlerFunc{ + // alive + func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/alive", r.URL.Path) + assert.Equal(t, fmt.Sprintf("https://%s/", host), r.Header.Get("Referer")) + assert.Equal(t, fmt.Sprintf("https://%s", host), r.Header.Get("Origin")) + assert.NotEmpty(t, r.URL.Query().Get("_")) + }, + func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/report", r.URL.Path) + assert.Equal(t, fmt.Sprintf("https://%s/", host), r.Header.Get("Referer")) + assert.Equal(t, fmt.Sprintf("https://%s", host), r.Header.Get("Origin")) + assert.Equal(t, "0dcfcbe4-5e20-47a3-9037-cb1d1bf4ad5b", r.URL.Query().Get("txid")) + assert.NotEmpty(t, r.URL.Query().Get("eh_service_url")) + }, + func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/frame/check_endpoint_app_status", r.URL.Path) + assert.Equal(t, host, r.Header.Get("Referer")) + assert.Equal(t, "0dcfcbe4-5e20-47a3-9037-cb1d1bf4ad5b", r.URL.Query().Get("txid")) + assert.Equal(t, "HAlorymR7ZuV2CxZz9T10jABE4jzkWFBJYLEnlF4nUwCqQ=|1683218343|2a46eb852b3ef9f662304cee03d008daed6d71f6", r.URL.Query().Get("sid")) + assert.Equal(t, "XMLHttpRequest", r.Header.Get("X-Requested-With")) + + }, + + func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/submit", r.URL.Path) + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, duoTX, r.URL.Query().Get("tx")) + assert.Equal(t, "2.8", r.URL.Query().Get("v")) + + assert.NoError(t, r.ParseForm()) + + assert.Equal(t, "HAlorymR7ZuV2CxZz9T10jABE4jzkWFBJYLEnlF4nUwCqQ=|1683218343|2a46eb852b3ef9f662304cee03d008daed6d71f6", r.Form.Get("sid")) + assert.Equal(t, "0dcfcbe4-5e20-47a3-9037-cb1d1bf4ad5b", r.Form.Get("txid")) + assert.Equal(t, "https://1.endpointhealth.duosecurity.com/v1/healthapp/device/health?_req_trace_group=fa4659be389f1c724121f27a_587a98dc11576a7ab8416a32", r.Form.Get("eh_service_url")) + assert.Equal(t, "UH7AkMtr7dqTzgeMefvQ", r.Form.Get("akey")) + assert.Equal(t, "15", r.Form.Get("response_timeout")) + assert.Equal(t, "https://login.example.com/signin/verify/duo/web", r.Form.Get("parent")) + assert.Equal(t, "https://127.0.0.1/report", r.Form.Get("duo_app_url")) + assert.Equal(t, "https://dl.duosecurity.com/DuoDeviceHealth-latest.pkg", r.Form.Get("eh_download_link")) + }, + } + + ts := httptest.NewTLSServer( + &testServer{handlers: handlers, t: t}) + defer ts.Close() + + oc, _ := setupTestClient(t, ts, "PUSH") + upstreamURL, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("couldn't parse URL: %v", err) + } + + doc, err := goquery.NewDocumentFromReader(strings.NewReader(fakeEndpointForm)) + if err != nil { + t.Fatalf("failed to validate document: %v, ", err) + } + + host = upstreamURL.Host + submitURL := ts.URL + "/submit" + + q := url.Values{} + q.Add("tx", "1234567890") + q.Add("parent", fmt.Sprintf("https://%s/signin/verify/duo/web", host)) + q.Add("v", "2.8") + + _, err = verifyEndpointHealth(oc, doc, host, host, host, submitURL, q) + if err != nil { + t.Fatalf("failed to verify endpoint health: %v", err) + } +} diff --git a/pkg/provider/okta/okta_webauthn.go b/pkg/provider/okta/okta_webauthn.go index 587671ef0..a35fd88c3 100644 --- a/pkg/provider/okta/okta_webauthn.go +++ b/pkg/provider/okta/okta_webauthn.go @@ -85,10 +85,10 @@ func (d *FidoClient) ChallengeU2F() (*SignedAssertion, error) { } request := &u2fhost.AuthenticateRequest{ Challenge: d.ChallengeNonce, - Facet: "https://" + d.AppID, + Facet: d.AppID, AppId: d.AppID, KeyHandle: d.KeyHandle, - WebAuthn: true, + WebAuthn: false, } // do the change prompted := false diff --git a/pkg/provider/okta/okta_webauthn_test.go b/pkg/provider/okta/okta_webauthn_test.go index 5daabbcd3..48b65fe72 100644 --- a/pkg/provider/okta/okta_webauthn_test.go +++ b/pkg/provider/okta/okta_webauthn_test.go @@ -45,7 +45,7 @@ func TestNewFidoClient(t *testing.T) { } } -func TestChallengeU2F(t *testing.T) { +func TestChallengeWebAuthnU2F(t *testing.T) { challengeNonce := "challengeNonce" appID := "appID" version := "version" @@ -66,12 +66,12 @@ func TestChallengeU2F(t *testing.T) { request := &u2fhost.AuthenticateRequest{ Challenge: challengeNonce, AppId: appID, - Facet: "https://" + appID, + Facet: appID, KeyHandle: keyHandle, ChannelIdPublicKey: nil, ChannelIdUnused: false, CheckOnly: false, - WebAuthn: true, + WebAuthn: false, } response := &u2fhost.AuthenticateResponse{} device.On("Authenticate", request).Return(response, nil) diff --git a/pkg/provider/onelogin/onelogin.go b/pkg/provider/onelogin/onelogin.go index c4b87b28e..28c024ba5 100644 --- a/pkg/provider/onelogin/onelogin.go +++ b/pkg/provider/onelogin/onelogin.go @@ -4,7 +4,7 @@ import ( "bytes" "encoding/json" "fmt" - "io/ioutil" + "io" "log" "net/http" "net/url" @@ -105,7 +105,7 @@ func (c *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) logger.Debug("Retrieved OneLogin OAuth token:", oauthToken) - authReq := AuthRequest{Username: loginDetails.Username, Password: loginDetails.Password, AppID: c.AppID, Subdomain: c.Subdomain} + authReq := AuthRequest{Username: loginDetails.Username, Password: loginDetails.Password, AppID: c.AppID, Subdomain: c.Subdomain, IPAddress: loginDetails.MFAIPAddress} var authBody bytes.Buffer err = json.NewEncoder(&authBody).Encode(authReq) if err != nil { @@ -131,7 +131,7 @@ func (c *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) } defer res.Body.Close() - body, err := ioutil.ReadAll(res.Body) + body, err := io.ReadAll(res.Body) if err != nil { return "", errors.Wrap(err, "error retrieving body from response") } @@ -184,7 +184,7 @@ func generateToken(oc *Client, loginDetails *creds.LoginDetails, host string) (s return "", errors.Wrap(err, "error retrieving oauth token response") } - body, err := ioutil.ReadAll(res.Body) + body, err := io.ReadAll(res.Body) if err != nil { return "", errors.Wrap(err, "error reading oauth token response") } @@ -272,7 +272,7 @@ func verifyMFA(oc *Client, oauthToken, appID, host, resp string) (string, error) return "", errors.Wrap(err, "error retrieving verify response") } - body, err := ioutil.ReadAll(res.Body) + body, err := io.ReadAll(res.Body) if err != nil { return "", errors.Wrap(err, "error retrieving body from response") } @@ -303,7 +303,7 @@ func verifyMFA(oc *Client, oauthToken, appID, host, resp string) (string, error) return "", errors.Wrap(err, "error retrieving token post response") } - body, err := ioutil.ReadAll(res.Body) + body, err := io.ReadAll(res.Body) if err != nil { return "", errors.Wrap(err, "error retrieving body from response") } @@ -348,7 +348,7 @@ func verifyMFA(oc *Client, oauthToken, appID, host, resp string) (string, error) return "", errors.Wrap(err, "error retrieving verify response") } - body, err := ioutil.ReadAll(res.Body) + body, err := io.ReadAll(res.Body) if err != nil { return "", errors.Wrap(err, "error retrieving body from response") } diff --git a/pkg/provider/onelogin/onelogin_test.go b/pkg/provider/onelogin/onelogin_test.go index f6a8f0eeb..97e797ebb 100644 --- a/pkg/provider/onelogin/onelogin_test.go +++ b/pkg/provider/onelogin/onelogin_test.go @@ -1,9 +1,16 @@ package onelogin_test import ( + "net/http" + "net/http/httptest" + "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/versent/saml2aws/v2/mocks" + "github.com/versent/saml2aws/v2/pkg/cfg" "github.com/versent/saml2aws/v2/pkg/creds" + "github.com/versent/saml2aws/v2/pkg/prompter" "github.com/versent/saml2aws/v2/pkg/provider" "github.com/versent/saml2aws/v2/pkg/provider/onelogin" ) @@ -38,3 +45,96 @@ func TestClient_Authenticate(t *testing.T) { }) } } + +func TestOneLoginSuccess(t *testing.T) { + svr := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.String(), "/auth/oauth2/v2/token") { + _, err := w.Write([]byte(` + { + "access_token": "accesstoken1" + } + `)) + assert.Nil(t, err) + } else if strings.HasPrefix(r.URL.String(), "/api/2/saml_assertion") { + _, err := w.Write([]byte(` + { + "message": "Success", + "data": "saml1" + } + `)) + assert.Nil(t, err) + } else { + t.Fatalf("unexpected %v", r) + } + })) + defer svr.Close() + idpAccount := cfg.NewIDPAccount() + idpAccount.URL = svr.URL + idpAccount.Username = "user@example.com" + idpAccount.SkipVerify = true + + loginDetails := &creds.LoginDetails{ + Username: idpAccount.Username, + Password: "abc123", + URL: idpAccount.URL, + } + + oc, err := onelogin.New(idpAccount) + assert.Nil(t, err) + resp, err := oc.Authenticate(loginDetails) + assert.Nil(t, err) + assert.Equal(t, "saml1", resp) +} + +func TestOneLoginMFA(t *testing.T) { + svr := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.String(), "/auth/oauth2/v2/token") { + _, err := w.Write([]byte(` + { + "access_token": "accesstoken1" + } + `)) + assert.Nil(t, err) + } else if strings.HasPrefix(r.URL.String(), "/api/2/saml_assertion/verify_factor") { + _, err := w.Write([]byte(` + { + "message": "Success", + "data": "saml1" + } + `)) + assert.Nil(t, err) + } else if strings.HasPrefix(r.URL.String(), "/api/2/saml_assertion") { + _, err := w.Write([]byte(` + { + "message": "MFA is required for this user", + "devices": [{"device_type": "Yubico YubiKey"}] + } + `)) + assert.Nil(t, err) + } else { + t.Fatalf("unexpected %v", r) + } + })) + defer svr.Close() + idpAccount := cfg.NewIDPAccount() + idpAccount.URL = svr.URL + idpAccount.MFA = onelogin.IdentifierYubiKey + idpAccount.Username = "user@example.com" + idpAccount.SkipVerify = true + + loginDetails := &creds.LoginDetails{ + Username: idpAccount.Username, + Password: "abc123", + URL: idpAccount.URL, + } + + pr := &mocks.Prompter{} + prompter.SetPrompter(pr) + pr.Mock.On("StringRequired", "Enter verification code").Return("5309") + + oc, err := onelogin.New(idpAccount) + assert.Nil(t, err) + resp, err := oc.Authenticate(loginDetails) + assert.Nil(t, err) + assert.Equal(t, "saml1", resp) +} diff --git a/pkg/provider/pingfed/pingfed.go b/pkg/provider/pingfed/pingfed.go index 16e6c7cc0..8618af0af 100644 --- a/pkg/provider/pingfed/pingfed.go +++ b/pkg/provider/pingfed/pingfed.go @@ -4,7 +4,7 @@ import ( "context" "encoding/base64" "fmt" - "io/ioutil" + "io" "net/http" "net/url" "time" @@ -73,7 +73,7 @@ func (ac *Client) follow(ctx context.Context, req *http.Request) (string, error) return "", errors.Wrap(err, "failed to build document from response") } - var handler func(context.Context, *goquery.Document) (context.Context, *http.Request, error) + var handler func(context.Context, *goquery.Document, *url.URL) (context.Context, *http.Request, error) if docIsFormRedirectToTarget(doc, ac.idpAccount.TargetURL) { logger.WithField("type", "saml-response-to-aws").Debug("doc detect") @@ -119,14 +119,14 @@ func (ac *Client) follow(ctx context.Context, req *http.Request) (string, error) return "", fmt.Errorf("Unknown document type") } - ctx, req, err = handler(ctx, doc) + ctx, req, err = handler(ctx, doc, res.Request.URL) if err != nil { return "", err } return ac.follow(ctx, req) } -func (ac *Client) handleLogin(ctx context.Context, doc *goquery.Document) (context.Context, *http.Request, error) { +func (ac *Client) handleLogin(ctx context.Context, doc *goquery.Document, _ *url.URL) (context.Context, *http.Request, error) { loginDetails, ok := ctx.Value(ctxKey("login")).(*creds.LoginDetails) if !ok { return ctx, nil, fmt.Errorf("no context value for 'login'") @@ -147,19 +147,26 @@ func (ac *Client) handleLogin(ctx context.Context, doc *goquery.Document) (conte return ctx, req, err } -func (ac *Client) handleOTP(ctx context.Context, doc *goquery.Document) (context.Context, *http.Request, error) { +func (ac *Client) handleOTP(ctx context.Context, doc *goquery.Document, requestURL *url.URL) (context.Context, *http.Request, error) { form, err := page.NewFormFromDocument(doc, "#otp-form") if err != nil { return ctx, nil, errors.Wrap(err, "error extracting OTP form") } + for _, v := range ac.client.Jar.Cookies(requestURL) { + if v.Name == ".csrf" { + form.Values.Set("csrfToken", v.Value) + break + } + } + token := prompter.StringRequired("Enter passcode") form.Values.Set("otp", token) req, err := form.BuildRequest() return ctx, req, err } -func (ac *Client) handleSwipe(ctx context.Context, doc *goquery.Document) (context.Context, *http.Request, error) { +func (ac *Client) handleSwipe(ctx context.Context, doc *goquery.Document, _ *url.URL) (context.Context, *http.Request, error) { form, err := page.NewFormFromDocument(doc, "#form1") if err != nil { return ctx, nil, errors.Wrap(err, "error extracting swipe status form") @@ -180,7 +187,7 @@ func (ac *Client) handleSwipe(ctx context.Context, doc *goquery.Document) (conte return ctx, nil, errors.Wrap(err, "error polling swipe status") } - body, err := ioutil.ReadAll(res.Body) + body, err := io.ReadAll(res.Body) if err != nil { return ctx, nil, errors.Wrap(err, "error parsing body from swipe status response") } @@ -208,7 +215,7 @@ func (ac *Client) handleSwipe(ctx context.Context, doc *goquery.Document) (conte return ctx, req, err } -func (ac *Client) handleRefresh(ctx context.Context, doc *goquery.Document) (context.Context, *http.Request, error) { +func (ac *Client) handleRefresh(ctx context.Context, doc *goquery.Document, _ *url.URL) (context.Context, *http.Request, error) { loginDetails, ok := ctx.Value(ctxKey("login")).(*creds.LoginDetails) if !ok { return ctx, nil, fmt.Errorf("no context value for login") @@ -223,7 +230,7 @@ func (ac *Client) handleRefresh(ctx context.Context, doc *goquery.Document) (con return ctx, req, err } -func (ac *Client) handleFormRedirect(ctx context.Context, doc *goquery.Document) (context.Context, *http.Request, error) { +func (ac *Client) handleFormRedirect(ctx context.Context, doc *goquery.Document, _ *url.URL) (context.Context, *http.Request, error) { form, err := page.NewFormFromDocument(doc, "") if err != nil { return ctx, nil, errors.Wrap(err, "error extracting redirect form") @@ -232,7 +239,7 @@ func (ac *Client) handleFormRedirect(ctx context.Context, doc *goquery.Document) return ctx, req, err } -func (ac *Client) handleWebAuthn(ctx context.Context, doc *goquery.Document) (context.Context, *http.Request, error) { +func (ac *Client) handleWebAuthn(ctx context.Context, doc *goquery.Document, _ *url.URL) (context.Context, *http.Request, error) { form, err := page.NewFormFromDocument(doc, "") if err != nil { return ctx, nil, errors.Wrap(err, "error extracting webauthn form") diff --git a/pkg/provider/pingfed/pingfed_test.go b/pkg/provider/pingfed/pingfed_test.go index 3d5897d23..4feb78760 100644 --- a/pkg/provider/pingfed/pingfed_test.go +++ b/pkg/provider/pingfed/pingfed_test.go @@ -3,14 +3,20 @@ package pingfed import ( "bytes" "context" - "io/ioutil" + "io" + "net/http" + "net/http/cookiejar" + "net/url" + "os" "testing" + "time" "github.com/PuerkitoBio/goquery" "github.com/stretchr/testify/require" "github.com/versent/saml2aws/v2/mocks" "github.com/versent/saml2aws/v2/pkg/creds" "github.com/versent/saml2aws/v2/pkg/prompter" + "github.com/versent/saml2aws/v2/pkg/provider" ) func TestMakeAbsoluteURL(t *testing.T) { @@ -53,7 +59,7 @@ var docTests = []struct { func TestDocTypes(t *testing.T) { for _, tt := range docTests { - data, err := ioutil.ReadFile(tt.file) + data, err := os.ReadFile(tt.file) require.Nil(t, err) doc, err := goquery.NewDocumentFromReader(bytes.NewReader(data)) @@ -74,16 +80,16 @@ func TestHandleLogin(t *testing.T) { } ctx := context.WithValue(context.Background(), ctxKey("login"), &loginDetails) - data, err := ioutil.ReadFile("example/login.html") + data, err := os.ReadFile("example/login.html") require.Nil(t, err) doc, err := goquery.NewDocumentFromReader(bytes.NewReader(data)) require.Nil(t, err) - _, req, err := ac.handleLogin(ctx, doc) + _, req, err := ac.handleLogin(ctx, doc, &url.URL{}) require.Nil(t, err) - b, err := ioutil.ReadAll(req.Body) + b, err := io.ReadAll(req.Body) require.Nil(t, err) s := string(b[:]) @@ -96,35 +102,51 @@ func TestHandleOTP(t *testing.T) { prompter.SetPrompter(pr) pr.Mock.On("StringRequired", "Enter passcode").Return("5309") - data, err := ioutil.ReadFile("example/otp.html") + data, err := os.ReadFile("example/otp.html") require.Nil(t, err) doc, err := goquery.NewDocumentFromReader(bytes.NewReader(data)) require.Nil(t, err) - ac := Client{} - _, req, err := ac.handleOTP(context.Background(), doc) + pingfedURL := &url.URL{ + Scheme: "https", + Host: "authenticator.pingone.com", + Path: "/pingid/ppm/auth/otp", + } + jar, err := cookiejar.New(&cookiejar.Options{}) + require.Nil(t, err) + jar.SetCookies(pingfedURL, []*http.Cookie{{ + Name: ".csrf", + Secure: true, + Expires: time.Now().Add(time.Hour * 24 * 30), + Value: "some-token", + }}) + + opts := &provider.HTTPClientOptions{IsWithRetries: false} + ac := Client{client: &provider.HTTPClient{Client: http.Client{Jar: jar}, Options: opts}} + _, req, err := ac.handleOTP(context.Background(), doc, pingfedURL) require.Nil(t, err) - b, err := ioutil.ReadAll(req.Body) + b, err := io.ReadAll(req.Body) require.Nil(t, err) s := string(b[:]) require.Contains(t, s, "otp=5309") + require.Contains(t, s, "csrfToken=some-token") } func TestHandleFormRedirect(t *testing.T) { - data, err := ioutil.ReadFile("example/form-redirect.html") + data, err := os.ReadFile("example/form-redirect.html") require.Nil(t, err) doc, err := goquery.NewDocumentFromReader(bytes.NewReader(data)) require.Nil(t, err) ac := Client{} - _, req, err := ac.handleFormRedirect(context.Background(), doc) + _, req, err := ac.handleFormRedirect(context.Background(), doc, &url.URL{}) require.Nil(t, err) - b, err := ioutil.ReadAll(req.Body) + b, err := io.ReadAll(req.Body) require.Nil(t, err) s := string(b[:]) @@ -133,17 +155,17 @@ func TestHandleFormRedirect(t *testing.T) { } func TestHandleWebAuthn(t *testing.T) { - data, err := ioutil.ReadFile("example/webauthn.html") + data, err := os.ReadFile("example/webauthn.html") require.Nil(t, err) doc, err := goquery.NewDocumentFromReader(bytes.NewReader(data)) require.Nil(t, err) ac := Client{} - _, req, err := ac.handleWebAuthn(context.Background(), doc) + _, req, err := ac.handleWebAuthn(context.Background(), doc, &url.URL{}) require.Nil(t, err) - b, err := ioutil.ReadAll(req.Body) + b, err := io.ReadAll(req.Body) require.Nil(t, err) s := string(b[:]) diff --git a/pkg/provider/pingone/example/selectdevicebutton.html b/pkg/provider/pingone/example/selectdevicebutton.html new file mode 100644 index 000000000..1861e5617 --- /dev/null +++ b/pkg/provider/pingone/example/selectdevicebutton.html @@ -0,0 +1,125 @@ + + + + + + Change Authenticating Device + + + + + + + +