Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
home:Algolia:OSS
openvpn-auth-okta
_service:obs_scm:openvpn-auth-okta-2.8.2.obscpio
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File _service:obs_scm:openvpn-auth-okta-2.8.2.obscpio of Package openvpn-auth-okta
07070100000000000041ED000000000000000000000002663BA04300000000000000000000000000000000000000000000002200000000openvpn-auth-okta-2.8.2/.circleci07070100000001000081A4000000000000000000000001663BA04300000659000000000000000000000000000000000000002D00000000openvpn-auth-okta-2.8.2/.circleci/config.ymlversion: 2.1 executors: golang: docker: - image: cimg/go:1.22 jobs: golangci-lint: executor: golang steps: - checkout # Download and cache dependencies - restore_cache: &restore-cache keys: - go-mod-{{ checksum "go.sum" }} - run: name: Install dependencies command: | go mod download - run: name: Go fmt command: | RES="$(gofmt -s -l .)" if [ -n "${RES}" ] then echo "${RES}" exit 1 fi - run: name: Install golangci-lint command: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.54.2 - run: name: GolangCI Lint command: golangci-lint run - save_cache: &save-cache paths: - /home/circleci/go/pkg/mod key: go-mod-{{ checksum "go.sum" }} test: executor: golang steps: - checkout # Download and cache dependencies - restore_cache: <<: *restore-cache - run: name: Install dependencies command: | go mod download - run: name: Test command: make test - run: name: Coverage report command: make coverage - store_artifacts: path: build/coverage.html - save_cache: <<: *save-cache workflows: lint_test_build: jobs: - golangci-lint - test: requires: - golangci-lint 07070100000002000041ED000000000000000000000002663BA04300000000000000000000000000000000000000000000002000000000openvpn-auth-okta-2.8.2/.github07070100000003000081A4000000000000000000000001663BA043000000A2000000000000000000000000000000000000002B00000000openvpn-auth-okta-2.8.2/.github/CODEOWNERS# This file list code owners for the repository, or part of it. # See https://help.github.com/articles/about-code-owners/ # Default owners * @algolia/foundation 07070100000004000081A4000000000000000000000001663BA04300000148000000000000000000000000000000000000003900000000openvpn-auth-okta-2.8.2/.github/PULL_REQUEST_TEMPLATE.md### What does this PR do? <!-- A brief description of the change being made with this pull request. --> ### Motivation <!-- What inspired you to submit this pull request? --> ### More - [ ] Added/updated tests - [ ] Added/updated documentation ### Additional Notes <!-- Anything else we should know when reviewing? --> 07070100000005000081A4000000000000000000000001663BA043000000E6000000000000000000000000000000000000002F00000000openvpn-auth-okta-2.8.2/.github/dependabot.ymlversion: 2 updates: - package-ecosystem: gomod directory: "/" schedule: interval: daily time: "05:20" open-pull-requests-limit: 10 reviewers: - algolia/foundation - algolia/security labels: - dependencies 07070100000006000081A4000000000000000000000001663BA043000000EC000000000000000000000000000000000000002300000000openvpn-auth-okta-2.8.2/.gitignore*~ *.pyc *.so *.o venv/ .DS_Store build/ okta_openvpn.ini api.ini .dccache # Temporary testing files testing/fixtures/validator/valid_control_file testing/fixtures/validator/invalid_control_file testing/fixtures/validator/control_file 07070100000007000081A4000000000000000000000001663BA0430000009E000000000000000000000000000000000000002400000000openvpn-auth-okta-2.8.2/AUTHORS.rst======= Authors ======= jeremy.jacque@algolia.com --------- Inspired by: --------- - https://github.com/jpf/okta-openvpn, written by joel.franusic@okta.com 07070100000008000081A4000000000000000000000001663BA04300004156000000000000000000000000000000000000002000000000openvpn-auth-okta-2.8.2/LICENSEMozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. 07070100000009000081A4000000000000000000000001663BA043000011C8000000000000000000000000000000000000002100000000openvpn-auth-okta-2.8.2/MakefileSHELL := bash .ONESHELL: .SHELLFLAGS := -eu -o pipefail -c MAKEFLAGS += --warn-undefined-variables MAKEFLAGS += --no-builtin-rules UNAME_S := $(shell uname -s) OSDESC := $(shell . /etc/os-release && echo $$ID) ifeq ($(OSDESC),raspbian) CGO := 1 else CGO := 0 endif INSTALL := install CC := gcc INC := -I. -I./build CFLAGS := -fPIC $(INC) -O2 -D_FORTIFY_SOURCE=2 -fstack-protector-strong LDFLAGS := -shared -fPIC DESTDIR := LIB_PREFIX := /usr/lib PLUGIN_DIR := openvpn/plugins BUILDDIR := build GOPLUGIN_LDFLAGS := -ldflags '-s -w -extldflags "-static"' GOPLUGIN_FLAGS := -trimpath -buildmode=pie -a $(GOPLUGIN_LDFLAGS) ifeq ($(UNAME_S),Linux) LIBOKTA_LDFLAGS := -ldflags '-s -w -extldflags -Wl,-soname,libokta-auth-validator.so' CPLUGIN_LDFLAGS := $(LDFLAGS) -Wl,-soname,openvpn-plugin-auth-okta.so else # MacOs X LIBOKTA_LDFLAGS := -ldflags '-s -w -extldflags -Wl,-install_name,libokta-auth-validator.so' CPLUGIN_LDFLAGS := $(LDFLAGS) -Wl,-install_name,openvpn-plugin-auth-okta.so endif LIBOKTA_FLAGS := -trimpath -buildmode=c-shared $(LIBOKTA_LDFLAGS) PKG_SRC := $(shell ls pkg/*/*.go | grep -v "_test.go") PLUGIN_DEPS := $(BUILDDIR)/libokta-auth-validator.so $(BUILDDIR)/openvpn-plugin-auth-okta.o openvpn-plugin.h all: binary plugin $(BUILDDIR): mkdir $(BUILDDIR) $(BUILDDIR)/%.o: %.c | $(BUILDDIR) $(CC) $(CFLAGS) -c $< -o $@ # Build the plugin as a standalone binary binary: $(BUILDDIR)/okta-auth-validator $(BUILDDIR)/okta-auth-validator: cmd/okta-auth-validator/main.go $(PKG_SRC) | $(BUILDDIR) CGO_ENABLED=$(CGO) go build $(GOPLUGIN_FLAGS) -o $(BUILDDIR)/okta-auth-validator cmd/okta-auth-validator/main.go # Build the openvpn-plugin-auth-okta plugin (linked against the Go c-shared lib) $(BUILDDIR)/openvpn-plugin-auth-okta.so: $(PLUGIN_DEPS) $(CC) $(CPLUGIN_LDFLAGS) -o $(BUILDDIR)/openvpn-plugin-auth-okta.so $(BUILDDIR)/openvpn-plugin-auth-okta.o # Build the okta-auth-validator shared lib (Golang c-shared) $(BUILDDIR)/libokta-auth-validator.so: lib/libokta-auth-validator.go $(PKG_SRC) | $(BUILDDIR) go build $(LIBOKTA_FLAGS) -o $(BUILDDIR)/libokta-auth-validator.so lib/libokta-auth-validator.go # Build all shared libraries plugin: $(BUILDDIR)/libokta-auth-validator.so $(BUILDDIR)/openvpn-plugin-auth-okta.so test: $(BUILDDIR)/cover.out coverage: $(BUILDDIR)/coverage.html # Run gobagde to update the README coverage badge after golang tests badge: $(BUILDDIR)/cover-badge.out if [ ! -f /tmp/gobadge ]; then \ curl -sf https://gobinaries.com/github.com/AlexBeauchemin/gobadge@v0.3.0 | PREFIX=/tmp sh; \ fi /tmp/gobadge -filename=$(BUILDDIR)/cover-badge.out # Run tests that generates the cover.out $(BUILDDIR)/cover.out: | $(BUILDDIR) # Ensure tests wont fail because of crappy permissions chmod -R g-w,o-w testing/fixtures go test ./pkg/... -v -cover -coverprofile=$(BUILDDIR)/cover.out -covermode=atomic -coverpkg=./pkg/... # Creates the coverage.html $(BUILDDIR)/coverage.html: $(BUILDDIR)/cover.out go tool cover -html=$(BUILDDIR)/cover.out -o $(BUILDDIR)/coverage.html # Creates the cover-badgeout (needed for README badge link creation) $(BUILDDIR)/cover-badge.out: $(BUILDDIR)/cover.out go tool cover -func=$(BUILDDIR)/cover.out -o=$(BUILDDIR)/cover-badge.out # You'll need to install golangci-lint and cppcheck # see https://github.com/danmar/cppcheck#packages # https://github.com/golangci/golangci-lint#install-golangci-lint lint: golangci-lint run cppcheck $(INC) --enable=all --disable=missingInclude --check-level=exhaustive *.c install: all mkdir -p $(DESTDIR)/$(LIB_PREFIX)/$(PLUGIN_DIR) mkdir -p $(DESTDIR)/etc/okta-auth-validator/ mkdir -p $(DESTDIR)/usr/include mkdir -p $(DESTDIR)/usr/bin $(INSTALL) -m755 $(BUILDDIR)/okta-auth-validator $(DESTDIR)/usr/bin/ $(INSTALL) -m644 $(BUILDDIR)/libokta-auth-validator.so $(DESTDIR)/$(LIB_PREFIX)/ $(INSTALL) -m644 $(BUILDDIR)/libokta-auth-validator.h $(DESTDIR)/usr/include/ $(INSTALL) -m644 $(BUILDDIR)/openvpn-plugin-auth-okta.so $(DESTDIR)/$(LIB_PREFIX)/$(PLUGIN_DIR)/ if [ ! -f $(DESTDIR)/etc/okta-auth-validator/pinset.cfg ]; then \ $(INSTALL) -m644 config/pinset.cfg $(DESTDIR)/etc/okta-auth-validator/pinset.cfg; \ fi if [ ! -f $(DESTDIR)/etc/okta-auth-validator/api.ini ]; then \ $(INSTALL) -m640 config/api.ini.inc $(DESTDIR)/etc/okta-auth-validator/api.ini; \ fi clean: rm -Rf $(BUILDDIR) rm -f testing/fixtures/validator/valid_control_file rm -f testing/fixtures/validator/invalid_control_file rm -f testing/fixtures/validator/control_file .PHONY: clean binary install lint badge coverage test plugin 0707010000000A000081A4000000000000000000000001663BA0430000206A000000000000000000000000000000000000002200000000openvpn-auth-okta-2.8.2/README.md![Release](https://img.shields.io/github/v/release/algolia/openvpn-auth-okta.svg) ![Go version](https://img.shields.io/github/go-mod/go-version/algolia/openvpn-auth-okta.svg) [![Go Reference](https://pkg.go.dev/badge/gopkg.in/algolia/openvpn-auth-okta.v2.svg)](https://pkg.go.dev/gopkg.in/algolia/openvpn-auth-okta.v2) ![CI status](https://circleci.com/gh/algolia/openvpn-auth-okta/tree/v2.svg?style=shield) ![Coverage](https://img.shields.io/badge/Coverage-97.6%25-brightgreen) [![Go Report Card](https://goreportcard.com/badge/gopkg.in/algolia/openvpn-auth-okta.v2)](https://goreportcard.com/report/gopkg.in/algolia/openvpn-auth-okta.v2) # Introduction This offers a set of lib and binary to authenticate users against [Okta Authentication API](https://developer.okta.com/docs/reference/api/authn/) , with support for MFA (TOTP or PUSH only). It also offers a plugin for OpenVPN (Community Edition) using the lib mentionned above. > :exclamation: Note: The plugin does not work with OpenVPN Access Server (OpenVPN-AS) # Requirements The plugin requires that OpenVPN Community Edition to be configured or used in one the following ways: 1. OpenVPN can be configured to call plugins via a deferred call (aka `Shared Object Plugin` mode) or call the binary directly (aka `Script Plugin` mode). 2. By default, OpenVPN clients *must* authenticate using client SSL certificates. 3. If authenticating requires MFA, the end user will authenticate by appending their six-digit MFA TOTP to the end of its password or by validating Push notifications. For TOTP, if a user's password is `correcthorsebatterystaple` and their six-digit MFA TOTP is `123456`, he should use `correcthorsebatterystaple123456` as the password for their OpenVPN client # Installation ## Install the Okta OpenVPN plugin You have three options to install the Okta OpenVPN plugin: ### 1. Use pre-built packages from repositories Thanks to the [OpenSUSE Build Service](https://build.opensuse.org/) packages are available for multiple distros: CentOS, Debian, Fedora, openSUSE, Ubuntu. Choose the proper instructions for your Linux distribution [here](https://software.opensuse.org/download/package?package=openvpn-auth-okta&project=home%3AAlgolia%3AOSS). ##### Packages are available for: - CentOS: `8`, `8 Stream` - Debian: `Bullseye` (11), `Bookworm` (12) - Fedora: `38`, `39` - openSUSE: `15.4`, `15.5` - Ubuntu: `Focal Fossa` (20.04), `Jammy Jellyfish` (22.04), `Lunar Lobster` (23.04), `Mantic Minotaur` (23.10) ### 2. For default setups, use `sudo make install` to run the install for you. Build requirements: - gcc - golang (>= 1.21) - make If you have a default OpenVPN setup, where plugins are stored in `/usr/lib/openvpn/plugins` and configuration files are stored in `/etc/okta-auth-validator`, then you can use the `make install` command to install the Okta OpenVPN plugin: ```shell $ sudo make install ``` ### 3. For custom setups, follow the manual installation instructions below. #### Compile the plugin Build requirements: - gcc - golang (>= 1.21) - make Compile the plugin from this directory using this command: ```shell $ make plugin ``` Compile the Golang binary from this repository using this command: ```shell $ make binary ``` #### Manually installing the Okta OpenVPN plugin If you have a custom setup, follow the instructions below to install the C plugin and Golang library that constitute the Okta OpenVPN plugin. #### Manually installing the C Plugin To manually install the C plugin, copy the `build/openvpn-plugin-auth-okta.so` file to the location where your OpenVPN plugins are stored and the `libokta-auth-validator.so` file to your system libdir. #### Manually installing the Golang binary To manually install the binary, copy the `okta-auth-validator` to your system bin dir; the `pinset.cfg`, and `api.ini` files to the location where your OpenVPN plugin scripts are stored. ## Make sure that OpenVPN has a tempory directory In OpenVPN, the "deferred plugin" model requires the use of temporary files to work. It is recommended that these temporary files are stored in a directory that only OpenVPN has access to. The default location for this directory is `/etc/openvpn/tmp`. If this directory doesn't exist, create it using this command: ```shell $ sudo mkdir /etc/openvpn/tmp ``` Use the [chown](https://en.wikipedia.org/wiki/Chown) and [chmod](https://en.wikipedia.org/wiki/Chmod) commands to set permissions approprate to your setup (The user that runs OpenVPN should be owner and only writer). # Configuration ## Configure the Okta OpenVPN plugin The Okta OpenVPN plugin is configured using the `api.ini` file. You **must** update this file with the configuration options for your Okta organization for the plugin to work. If you installed the Okta OpenVPN plugin to the default location, run this command to edit your configuration file. ```shell $ sudo $EDITOR /etc/okta-auth-validator/api.ini ``` > :warning: As this file contains your Okta token, please ensure it has limited permissions (should only be readable by root or the user running OpenVPN) ! See [api.ini](https://github.com/algolia/openvpn-auth-okta/blob/v2/api.ini.inc) for configuration options. ## Configure OpenVPN to use the C `Shared Object Plugin` Set up OpenVPN to call the Okta plugin by adding the following lines to your OpenVPN `server.conf` configuration file: ```ini plugin openvpn-plugin-auth-okta.so tmp-dir "/etc/openvpn/tmp" ``` The default location for the OpenVPN configuration file is `/etc/openvpn/server.conf`. This method is considered the safest as no credential is exported to a process environment or written to disk. ## Configure OpenVPN to use the binary in `Script Plugin` mode Set up OpenVPN to call the Golang binary by adding the following lines to your OpenVPN `server.conf` configuration file: ```ini # "via-file" method auth-user-pass-verify /usr/bin/okta-auth-validator via-file tmp-dir "/etc/openvpn/tmp" ``` > :exclamation: it is strongly advised when using the via file method, that the tmp-dir is located on a tmpfs filesystem (so that the user's credentials never reach the disk). Systemd can help for that: ```shell VUSER=openvpn echo "d /run/openvpn/tmp 1750 ${VUSER} root" | sudo tee /etc/tmpfiles.d/openvpn-tmp.conf sudo systemd-tmpfiles --create /etc/tmpfiles.d/openvpn-tmp.conf ``` ```ini # "via-env" method auth-user-pass-verify /usr/bin/okta-auth-validator via-env tmp-dir "/etc/openvpn/tmp" ``` Please check the OpenVPN [manual](https://openvpn.net/community-resources/reference-manual-for-openvpn-2-0/#options) for security considerations regarding this mode. # Log outputs Outputs have been designed to be easily parsable, you'll find 2 different formats depending on wether the username has been set or not, ie: Before ``` Thu Dec 21 03:41:28 2023 [okta-auth-validator:4dd5f892-c51d-43bf-94c7-87b25b81707e](ERROR): Initpool failure ``` After ``` Thu Dec 21 03:41:28 2023 [okta-auth-validator:50bc833a-dcea-4337-9d73-41af17371c4e](INFO): [dade.murphy@example.com] Authenticating ``` A grok pattern could be: ``` DATESTAMP_OKTA %{DAY} %{MONTH} %{MONTHDAY} %{TIME} %{YEAR} %{DATESTAMP_OKTA:timestamp} \[okta-auth-validator:%{UUID:session_id}\]\(%{LOGLEVEL:level}\):(%{SPACE}\[((%{EMAILADDRESS:username})|(%{EMAILLOCALPART:username}))\])? %{GREEDYDATA:message} ``` # Useful links - [OpenVPN: Using alternative authentication methods](https://openvpn.net/community-resources/using-alternative-authentication-methods/) - [OpenVPN 2.4 manual](https://openvpn.net/community-resources/reference-manual-for-openvpn-2-4/) - [Openvpn multi-auth sample plugin code](https://github.com/OpenVPN/openvpn/blob/master/sample/sample-plugins/defer/multi-auth.c) - [Okta API - PreAuth](https://developer.okta.com/docs/reference/api/authn/#primary-authentication-with-public-application) - [Okta API - Auth with TOTP MFA](https://developer.okta.com/docs/reference/api/authn/#verify-totp-factor) - [Okta API - Auth with Push MFA](https://developer.okta.com/docs/reference/api/authn/#verify-push-factor) # Contact Updates or corrections to this document are very welcome. Feel free to send me [pull requests](https://help.github.com/articles/using-pull-requests/) with suggestions or open issues. 0707010000000B000041ED000000000000000000000003663BA04300000000000000000000000000000000000000000000001C00000000openvpn-auth-okta-2.8.2/cmd0707010000000C000041ED000000000000000000000002663BA04300000000000000000000000000000000000000000000003000000000openvpn-auth-okta-2.8.2/cmd/okta-auth-validator0707010000000D000081A4000000000000000000000001663BA043000004FF000000000000000000000000000000000000003800000000openvpn-auth-okta-2.8.2/cmd/okta-auth-validator/main.go// SPDX-FileCopyrightText: 2023-Present Algolia // // SPDX-License-Identifier: MPL-2.0 // // Copyright 2023-Present Algolia // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. /* CLI tool to test/request Okta MFA validation */ package main import ( "flag" "os" "gopkg.in/algolia/openvpn-auth-okta.v2/pkg/validator" ) var ( debug *bool deferred *bool ) type OktaOpenVPNValidator = validator.OktaOpenVPNValidator func main() { logLevel := "INFO" debug = flag.Bool("d", false, "enable debugging") trace := flag.Bool("dd", false, "enable heavy debugging") deferred = flag.Bool("deferred", false, "does this run as a deferred OpenVPN plugin") flag.Parse() args := flag.Args() if *debug { logLevel = "DEBUG" } if *trace { logLevel = "TRACE" } oktaValidator := validator.New(logLevel) if res := oktaValidator.Setup(*deferred, args, nil); !res { if *deferred { os.Exit(0) } else { os.Exit(1) } } err := oktaValidator.Authenticate() if *deferred { oktaValidator.WriteControlFile() os.Exit(0) } // from here, in "Script Plugins" mode if err == nil { os.Exit(0) } os.Exit(1) } 0707010000000E000041ED000000000000000000000002663BA04300000000000000000000000000000000000000000000001F00000000openvpn-auth-okta-2.8.2/config0707010000000F000081A4000000000000000000000001663BA0430000062F000000000000000000000000000000000000002B00000000openvpn-auth-okta-2.8.2/config/api.ini.inc[General] ## Set the out log level ## Allowed values: TRACE, DEBUG, INFO, WARN, ERROR ## (Optional, default: INFO) LogLevel: INFO [OktaAPI] ## The URL for your Okta instance ## (Required) # Url: https://example.okta.com Url: ## The API Token for your Okta instace ## (Required) # Token: 01Abcd2efGHIjKl3m4NoPQrstu5vwxYZ_AbcdefGHi Token: ## A suffix to be appended to the end of user names ## before the attempting authentication against Okta. ## For example: If this was set to 'example.com', a user with a ## certificate / username identifiying them as 'first.last' would be authenticated ## against Okta as 'first.last@example.com'. ## (Optional, default: "") # UsernameSuffix: example.com ## Do not require usernames to come from client-side SSL certificates. ## NOT RECCOMMENDED FOR PRODUCTION ENVIRONMENTS ## (Optional, default: False) # AllowUntrustedUsers: True ## Configure what groups are allowed to connect to server (comma separated list) ## (Optional, default: "") # AllowedGroups: vpnusers, developers ## Always request MFA validation (TOTP or PUSH) ## If PreAuth is successful without MFA challenge, user will be rejected ## (Optional, default: False) # MFARequired: True ## Configure how many retries to poll Okta for results of an Okta Verify Push are allowed ## (Optional, default: 20) # MFAPushMaxRetries: 20 ## Configure how many seconds to wait between Okta Verify Push retries ## (Optional, default: 3) # MFAPushDelaySeconds: 3 # If a passcode is provided and TOTP MFA fails, try Push MFA ## (Optional, default: False) TOTPFallbackToPush: False 07070100000010000081A4000000000000000000000001663BA043000003F2000000000000000000000000000000000000002A00000000openvpn-auth-okta-2.8.2/config/pinset.cfg# # Here is how a pin like those below may be generated: # echo -n | openssl s_client -connect example.com:443 | # openssl x509 -noout -pubkey | # openssl rsa -pubin -outform der | # openssl dgst -sha256 -binary | base64 # okta.com r5EfzZxQVvQpKo3AgYRaT7X2bDO/kj3ACwmxfdT2zt8= MaqlcUgk2mvY/RFSGeSwBRkI+rZ6/dxe/DuQfBT/vnQ= 72G5IEvDEWn+EThf3qjR7/bQSWaS2ZSLqolhnO6iyJI= rrV6CLCCvqnk89gWibYT0JO6fNQ8cCit7GGoiVTjCOg= # oktapreview.com jZomPEBSDXoipA9un78hKRIeN/+U4ZteRaiX8YpWfqc= axSbM6RQ+19oXxudaOTdwXJbSr6f7AahxbDHFy3p8s8= SE4qe2vdD9tAegPwO79rMnZyhHvqj3i5g1c2HkyGUNE= ylP0lMLMvBaiHn0ihLxHjzvlPVQNoyQ+rMiaj0da/Pw= # internal testing W2qOJ9F9eo3CYHzL5ZIjYEizINI1cUPEb7yD45ihTXg= PJ1QGTlW5ViFNhswMsYKp4X8C7KdG8nDW4ZcXLmYMyI= 5LlRWGTBVjpfNXXU5T7cYVUbOSPcgpMgdjaWd/R9Leg= lpaMLlEsp7/dVZoeWt3f9ciJIMGimixAIaKNsn9/bCY= # internal testing Uit61pzomPOIy0svL1z4OUx3FMBr9UWQVdyG7ZlSLK8= Ul2vkypIA80/JDebYsXq8FGdtmtrx5WJAAHDlSwWOes= rx1UuNLIkJs53Jd60G/zY947XcDIf56JyM/yFJyR/GE= VvpiE4cl60BvOU8X4AfkWeUPsmRUSh/nVbJ2rnGDZHI= 07070100000011000041ED000000000000000000000003663BA04300000000000000000000000000000000000000000000001F00000000openvpn-auth-okta-2.8.2/debian07070100000012000081A4000000000000000000000001663BA043000046F1000000000000000000000000000000000000002900000000openvpn-auth-okta-2.8.2/debian/changelogopenvpn-auth-okta (2.8.2) stable; urgency=medium [ dependabot[bot] ] * chore(deps): bump github.com/stretchr/testify from 1.8.4 to 1.9.0 * chore(deps): bump github.com/go-playground/validator/v10 * chore(deps): bump github.com/phuslu/log from 1.0.88 to 1.0.89 * chore(deps): bump github.com/phuslu/log from 1.0.89 to 1.0.90 * chore(deps): bump golang.org/x/net from 0.21.0 to 0.23.0 * chore(deps): bump github.com/phuslu/log from 1.0.90 to 1.0.91 * chore(deps): bump github.com/go-playground/validator/v10 * chore(deps): bump github.com/phuslu/log from 1.0.91 to 1.0.92 * chore(deps): bump github.com/phuslu/log from 1.0.92 to 1.0.93 * chore(deps): bump github.com/phuslu/log from 1.0.93 to 1.0.96 * chore(deps): bump github.com/phuslu/log from 1.0.96 to 1.0.97 [ Jeremy JACQUE ] * feat(lib): allow to use a struct for PluginEnv * feat(lib): provide a way to compute lib args from plugin envp, args passed as a struct * fix(Makefile): use proper inc dirs for gcc and cppcheck * feat(lib): rename the C function computing args * doc: add some comments * fix(lib): gotfmt * chore(lib): let the user allocate and free himself the ArgsOktaAuthValidatorV2 struct * refacto(plugin): mutualize error code during lib related calls * chore(cmd): add missing license header -- Jeremy Jacque <jeremy.jacque@algolia.com> Tue, 07 May 2024 12:47:00 +0000 openvpn-auth-okta (2.8.1) stable; urgency=medium [ dependabot[bot] ] * chore(deps): bump github.com/google/uuid from 1.5.0 to 1.6.0 * chore(deps): bump github.com/go-playground/validator/v10 [ Jeremy JACQUE ] * chore(ci): check go fmt * fix(cmd): gofmt -- Jeremy Jacque <jeremy.jacque@algolia.com> Mon, 26 Feb 2024 08:28:08 +0000 openvpn-auth-okta (2.8.0) stable; urgency=medium [ dependabot[bot] ] * chore(deps): bump golang.org/x/crypto from 0.7.0 to 0.17.0 [ dependabot[bot] ] * chore(deps): bump golang.org/x/net from 0.8.0 to 0.17.0 [ Jeremy JACQUE ] * fix(oktaApiAuth): do not return "valid" http 500 on request error * chore: add comments and basic traces * chore(oktaApiAuth): add validity checks on Okta groups * feat(validator): allow various log level and to set it up in the conf file * chore(doc): update coverage badge * chore: normalize validator constructor name * chore: normalize oktaApiAuth constructor name * chore(validator): if log level has been set in constructor, use it as default if not provided in config * refacto: use phuslu/log instead of logrus for better perf/memory usage * chore(go): clean sums -- Jeremy Jacque <jeremy.jacque@algolia.com> Sun, 25 Feb 2024 21:56:40 +0000 openvpn-auth-okta (2.7.0) stable; urgency=medium [ Jeremy JACQUE ] * chore(debian): allow building with go 1.22 * chore(go): add go-playground/validator for API response validation * chore(go): use proper json unmarshaling/validation instead of string parsing for API responses * refacto(oktaApiAuth): factorize common (first step) MFA verification code * chore(oktaApiAuth/test): test MFA with multiple push retries * refacto(oktaApiAuth/test): split TestAuth function * chore(oktaApiAuth) respect API spec anf honor HTTP codes * chore(oktaApiAuth/test): respect API spec anf honor HTTP codes * chore(oktaApiAuth): factorize TOTP and Push MFA verification code * refacto(oktaApiAuth): dedup and simplify doAuthFirstStep code * fix(oktaApiAuth): Okta will never answer success at first push MFA verify call * fix(oktaApiAuth): status is mandatory in a AuthResponse * fix(oktaApiAuth): add missing cancel in verifyFactors, simplify if/else with return * fix(oktaApiAuth): return proper error for checkAllowedGroups HTTP error * chore(oktaApiAuth/test): cover all Auth scenarii * chore(oktaApiAuth/test): increase coverage of CheckAllowedGroups * chore(doc): update coverage badge * chore(oktaApiAuth/test): add auth_invalid_totp_no_sum fixture file * chore(oktaApiAuth): cancelAuth is a fire & forget, no need for return values * chore(oktaApiAuth): add some debug messages * chore(doc): update coverage badge * fix(oktaApiAuth): reset AuthResponse at each loop iteration to prevent persistency issues * chore: make TOTP to Push fallback configurable * refacto: simplify code - remove else when not needed -- Jeremy Jacque <jeremy.jacque@algolia.com> Thu, 22 Feb 2024 04:13:56 +0000 openvpn-auth-okta (2.6.1) stable; urgency=medium [ Jeremy JACQUE ] * chore(go): fmt * chore(ci): remove snyk jobs as we are decommissioning it * chore(doc): simplify pkg install section by using OBS instruction page * chore: add license header to source files -- Jeremy Jacque <jeremy.jacque@algolia.com> Fri, 09 Feb 2024 21:33:15 +0000 openvpn-auth-okta (2.6.0) stable; urgency=medium [ Jeremy JACQUE ] * fix(plugin): add missing dlclose * chore(Makefile): minimize built file size and trimpath * chore(Makefile): clean unused vars and targets, tune go build params * refacto(go): simplify logging by using different fomatter before and after username is set * chore(doc): update coverage badge * chore(doc): add a section about logs * refacto(oktaApiAuth): split oktaApiAuth package into multiple files * refacto(oktaApiAuth/test): split oktaApiAuth tests into multiple files * refacto(validator): integrate utils pkg as it is only used by validator * refacto(validator): split validator package into multiple files * refacto(validator/test): split validator tests into multiple files * refacto(Makefile): use a shell cmd to get the full list of pkg files for dependencies * fix(Makefile): GOLDFLAGS has been renamed GOPLUGIN_LDFLAGS in a previous commit * refacto(oktaApiAuth/test): use types_test.go only for stuff common to multiple tests * refacto(validator): move checkControlFilePerm to utils(_test) * chore(oktaApiAuth): add a todo for Pool only used in tests -- Jeremy Jacque <jeremy.jacque@algolia.com> Fri, 22 Dec 2023 06:56:20 +0000 openvpn-auth-okta (2.5.10) stable; urgency=medium [ Jeremy JACQUE ] * chore(oktaApiAuth): rephrase/enrich log outputs * chore(validator): rephrase/enrich log outputs * chore: create a dedicated func for logging setup (formatting with uuid, log-level) * chore(go): add uuid package needed for logging * chore(tools): update url for https://toolkit.okta.com/apps/ -- Jeremy Jacque <jeremy.jacque@algolia.com> Tue, 19 Dec 2023 02:45:59 +0000 openvpn-auth-okta (2.5.9) stable; urgency=medium [ Jeremy JACQUE ] * refacto(oktaApiAuth): handle totp and push MFA sequentialy with different error msg * chore(oktaApiAuth/test): rename MFA errors, add test for invalid response for totp auth * fix(oktaApiAuth): continue outer loop when push triggers error and not last * chore(oktaApiAuth/test): test if multiple TOTP or push providers are possible * chore(doc): update coverage badge * refacto(oktaApiAuth): split validateUserMFA with 2 more functions (TOTP and push) * chore(oktaApiAuth/test): implement tests following refacto -- Jeremy Jacque <jeremy.jacque@algolia.com> Mon, 18 Dec 2023 01:36:45 +0000 openvpn-auth-okta (2.5.8) stable; urgency=medium [ Jeremy JACQUE ] * fix(oktaApiAuth): user may have multiple OTP providers, try all -- Jeremy Jacque <jeremy.jacque@algolia.com> Sat, 16 Dec 2023 13:13:50 +0000 openvpn-auth-okta (2.5.7) stable; urgency=medium [ Jeremy JACQUE ] * fix(oktaApiAuth): handle properly transaction cancelation, only when needed * fix(oktaApiAuth/test): fix with new calls to transaction cancellation * chore(doc): update coverage badge * chore(oktaApiAuth): homogenize / clean auth logs * fix(oktaApiAuth): handle properly TOTP error (invalid passcode) * chore(oktaApiAuth): simplify validateMFA signature * chore(oktaApiAuth): rename some functions/vars for better readability * fix(oktaApiAuth): respect MFAPushMaxRetries count, saves an API call/MFAPushDelaySeconds sleep * feat(oktaApiAuth): handle expired password (only when no active MFA) * chore(config): clean a bit api.ini config template * chore(doc): rearrange and fix typo in README * fix(dist): add missing packages in dsc -- Jeremy Jacque <jeremy.jacque@algolia.com> Sat, 16 Dec 2023 07:36:56 +0000 openvpn-auth-okta (2.5.6) stable; urgency=medium [ Jeremy JACQUE ] * style: gofmt some source files * refacto(oktaApiAuth): reduce gocyclo score of Auth to an acceptable score * chore(oktaApiAuth): homogenize some Auth logs * chore(oktaApiAuth/test): add a test for invalid preauth response * chore(doc): update coverage badge -- Jeremy Jacque <jeremy.jacque@algolia.com> Fri, 15 Dec 2023 12:50:53 +0000 openvpn-auth-okta (2.5.5) stable; urgency=medium [ Jeremy JACQUE ] * chore(validator): add msg for setup failures * chore: use logrus to have a clean output * fix(Makefile): missing source deps in targets * chore(doc): update coverage badge * fix(Makefile): allow to build on MacOS * chore(validator): handle properly new config files names * chore(Makefile): add proper MacOS ldflags for libs * chore(oktaApiAuth): allow POST and GET in oktaReq * feat(oktaApiAuth): check if user is a member of an AllowedGroup * feat((oktaApiAuth/test): test checkAllowedGroups functions * feat(config): add AllowedGroups option * chore(doc): update coverage badge * feat(oktaApiAuth/test): add invalid payload test * chore(validator/test): check config file detection * chore(validator/test): test wrongly formatted ini file * chore(doc): update coverage badge * chore(github): remove refs to pip in dependabot -- Jeremy Jacque <jeremy.jacque@algolia.com> Thu, 14 Dec 2023 21:09:10 +0000 openvpn-auth-okta (2.5.4) stable; urgency=medium [ Jeremy JACQUE ] * chore: rename config files * fix(doc): fix invalid license header * chore(doc): small README updates * chore(doc): remove CODE.md as the pkg is now referenced in pkg.go.dev * chore(doc): update authors * chore(dist): move tool to update packages version * chore(config): move config files to a dedicated dir * chore(Makefile): enforce some compiler options * fix(debian): ensure proper config files permissions -- Jeremy Jacque <jeremy.jacque@algolia.com> Thu, 26 Oct 2023 22:20:24 +0000 openvpn-auth-okta (2.5.3) stable; urgency=medium [ Jeremy JACQUE ] * style(utils): use gofmt std indentation * style(oktaApiAuth): use gofmt std indentation * style(validator): use gofmt std indentation * style(cmd): use gofmt std indentation * style(lib): use gofmt std indentation * chore(doc): update coverage badge * chore(doc): add doc ref and goreportcard badges * chore(pkg): rename config files * chore(Makefile): rename config files * chore(dist): rename config files * chore(doc): rename config files * chore(git): rename config files -- Jeremy Jacque <jeremy.jacque@algolia.com> Wed, 25 Oct 2023 08:28:10 +0000 openvpn-auth-okta (2.5.2) stable; urgency=medium [ Jeremy JACQUE ] * chore(oktaApiAuth): add comments for functions * chore(utils): add comments for functions * chore(validator): remove useless function * chore(validator): add comments for functions * chore(validator): do not set clientIp when not needed * chore(oktaApiAuth): clientIp is not set anymore to 0.0.0.0 * chore(oktaApiAuth): add struct comments * chore(validator): add struct comments -- Jeremy Jacque <jeremy.jacque@algolia.com> Wed, 25 Oct 2023 01:38:42 +0000 openvpn-auth-okta (2.5.1) stable; urgency=medium [ Jeremy JACQUE ] * chore(pkg): relocate http pool initialisation from utils to oktaApiAuth * chore(utils): simplify test * chore(utils): add a function to remove comments from a slice * chore(validator): remove from the pinset list empty lines and comments * chore(utils): simplify CheckUsernameFormat -- Jeremy Jacque <jeremy.jacque@algolia.com> Tue, 24 Oct 2023 01:03:42 +0000 openvpn-auth-okta (2.5.0) stable; urgency=medium [ Jeremy JACQUE ] * chore(pkg): remove now useless types package * chore(oktaApiAuth): move types in pkg, split constructor, make needed struct fields public * chore(oktaApiAuth/test): struct has been renamed, test InitPool * chore(validator): move types in pkg, parse for passcode here, move okta api related struct * chore(validator/test): adapt to latest changes, add more tests * chore(lib): adapt to struct moves * chore(testing): add new fixture for validator * chore(doc): reflect latest changes * chore(validator): remove useles comments, error test * chore(doc): update coverage badge after new tests implem * chore(debian): add missing common package for config files * chore(dist): split RPM pkg to have a dedicated config file one * chore(tools): small fix * fix(dist): add missing description for okta-auth-validator-common * chore(dist): fix some rpmlint issues * chore(cfg): remove algolia ref -- Jeremy Jacque <jeremy.jacque@algolia.com> Sun, 22 Oct 2023 23:37:45 +0000 openvpn-auth-okta (2.4.3) stable; urgency=medium [ Jeremy JACQUE ] * chore: change plugin file name to be aligned with other OpenVPN plugins * chore(doc): fix typo in install section and add info about OpenVPN tmp dir * chore(plugin): rename source according to new plugin file name * chore(doc): update with new plugin name * chore(git): now all produced files are in a dedicated dir, so ignore this dir -- Jeremy Jacque <jeremy.jacque@algolia.com> Fri, 20 Oct 2023 07:36:46 +0000 openvpn-auth-okta (2.4.2) stable; urgency=medium [ Jeremy JACQUE ] * chore(validator): refacto to remove useless functions and better err control on Authenticate * chore(cmd/okta-auth-validator): adapt to new Authenticate return type * chore(lib): adapt to new Authenticate return type * chore(validator/test): test new Authenticate err returned * chore(oktaApiAuth/test): test the case where we need to sort the offered MFAs * fix(validator/test): handle possible nil error * chore(oktaApiAuth/test): test the case where preAuth fails (connection issue) * chore(fixtures): add preauth_mfa_required_multi used by oktaApiAuth/TestAuth * chore(doc): update coverage badge after new tests implem -- Jeremy Jacque <jeremy.jacque@algolia.com> Fri, 20 Oct 2023 02:28:46 +0000 openvpn-auth-okta (2.4.1) stable; urgency=medium [ Jeremy JACQUE ] * chore: renamed repo and packages * chore(github): add code owners and PR template * chore(Makefile): change config files location * chore(doc): change config files location * chore(dist): change config files location * chore(validator): change default config files location -- Jeremy Jacque <jeremy.jacque@algolia.com> Thu, 19 Oct 2023 14:19:03 +0000 openvpn-auth-okta (2.4.0) stable; urgency=medium [ Jeremy JACQUE ] * chore(debian): split package * chore(dist): split package * chore(dist): update rpmlintrc filters * fix(tools): work on current branch to generate changelogs -- Jeremy Jacque <jeremy.jacque@algolia.com> Thu, 19 Oct 2023 07:47:08 +0000 openvpn-auth-okta (2.3.4) stable; urgency=medium [ Jeremy JACQUE ] * chore: remove unneeded defer_simple source code * chore(doc): add section about package installation * chore(dist): remove refs to defer_simple in spec file -- Jeremy Jacque <jeremy.jacque@algolia.com> Wed, 18 Oct 2023 22:16:12 +0000 openvpn-auth-okta (2.3.3) stable; urgency=medium * chore(Makefile): add missing soname to shared lib * chore(Makefile): binary should bo to /usr/bin * chore(dist): add files needed to build packages * chore(dist): add script to manage versions * chore(debian): Update changelog for 2.3.2 release * chore(dist): Update changelog for 2.3.2 release * chore(debian): add source format * fix(Makefile): create bin dir before install * fix(debian): wrong package format * chore(defer_okta_openvpn): fix indent, do not store useless pointer in context * chore: rename function for clarity * chore(Makefile): DESTDIR should be empty by default * chore: rename okta_openvpn to okta-auth-validator to be more generic * fix(lib): wrong (previous) export function name * chore: rename C source file * chore: rename lib to be more generic * chore(oktaApiAuth): do not flood openvpn logs with useless messages * chore(dist): Update changelog for 2.3.3 release -- Jeremy JACQUE <jeremy.jacque@algolia.com> Wed, 18 Oct 2023 14:01:30 +0000 openvpn-auth-okta (2.3.2) stable; urgency=medium [ Jeremy JACQUE ] * chore(Makefile): symlink is not needed as dlopen follows ld.so * chore(Makefile): add missing soname to shared lib * chore(Makefile): binary should bo to /usr/bin * chore(dist): add files needed to build packages * chore(dist): add script to manage versions -- Jeremy Jacque <jeremy.jacque@algolia.com> Wed, 18 Oct 2023 08:32:24 +0000 openvpn-auth-okta (2.3.1-algolia) stable; urgency=medium * Version 2.3.1: Fix loading of Golang c-shared in plugin (use dlopen) Use a dedicated struct instead of exporting to env user infos when using shared plugin for security reasons -- Jeremy Jacque <jeremy.jacque@algolia.com> Mon, 13 Oct 2023 04:04:41 +0200 openvpn-auth-okta (2.3.0-algolia) stable; urgency=medium * Version 2.3.0: Implement "full" shared lib plugin -- Jeremy Jacque <jeremy.jacque@algolia.com> Mon, 13 Oct 2023 04:04:41 +0200 openvpn-auth-okta (2.2.3-algolia) stable; urgency=medium * Version 2.2.3: Fix makefile and debian files -- Jeremy Jacque <jeremy.jacque@algolia.com> Sat, 14 Oct 2023 14:31:41 +0200 openvpn-auth-okta (2.2.2-algolia) stable; urgency=medium * Version 2.2.2: Handle all OpenVPN auth plugin modes and methods Add testing for almost all packages functions Updated doc (README, AUTHORS, CODE) -- Jeremy Jacque <jeremy.jacque@algolia.com> Fri, 13 Oct 2023 22:51:41 +0200 openvpn-auth-okta (2.1.1-algolia) stable; urgency=medium * Version 2.1.1: First Golang implementation Fix defer_plugin (passing script instead of argv to execve) Updated Makefile and debian files -- Jeremy Jacque <jeremy.jacque@algolia.com> Mon, 9 Oct 2023 05:30:41 +0200 07070100000013000081A4000000000000000000000001663BA04300000003000000000000000000000000000000000000002600000000openvpn-auth-okta-2.8.2/debian/compat10 07070100000014000081A4000000000000000000000001663BA0430000065A000000000000000000000000000000000000002700000000openvpn-auth-okta-2.8.2/debian/controlSource: openvpn-auth-okta Section: net Priority: optional Maintainer: Jeremy Jacque <jeremy.jacque@algolia.com> Build-Depends: debhelper (>=10), make, gcc, golang-1.22 Standards-Version: 0.1 Homepage: https://github.com/algolia/openvpn-auth-okta Package: openvpn-auth-okta Architecture: any Pre-Depends: ${misc:Pre-Depends} Depends: ${misc:Depends}, ${shlibs:Depends}, libokta-auth-validator (= ${source:Version}), okta-auth-validator-common (= ${source:Version}) Description: This is a plugin for OpenVPN (Community Edition) that authenticates users directly against Okta, with support for MFA. Package: okta-auth-validator Architecture: any Pre-Depends: ${misc:Pre-Depends} Depends: ${misc:Depends}, ${shlibs:Depends}, libokta-auth-validator (= ${source:Version}), okta-auth-validator-common (= ${source:Version}) Description: This is a command line tool that authenticates users directly against Okta, with support for MFA. Package: libokta-auth-validator Architecture: any Pre-Depends: ${misc:Pre-Depends} Depends: ${misc:Depends}, ${shlibs:Depends}, okta-auth-validator-common (= ${source:Version}) Suggests: libokta-auth-validator-dev Description: Shared library that allows to authenticates user directly against Okta, with support for MFA. Package: libokta-auth-validator-dev Architecture: all Pre-Depends: ${misc:Pre-Depends} Depends: ${misc:Depends}, ${shlibs:Depends} Description: Development files for libokta-auth-validator. Package: okta-auth-validator-common Architecture: all Pre-Depends: ${misc:Pre-Depends} Depends: ${misc:Depends}, ${shlibs:Depends} Description: Config files for okta-auth-validator. 07070100000015000081A4000000000000000000000001663BA04300000025000000000000000000000000000000000000004200000000openvpn-auth-okta-2.8.2/debian/libokta-auth-validator-dev.installusr/include/libokta-auth-validator.h 07070100000016000081A4000000000000000000000001663BA04300000024000000000000000000000000000000000000003E00000000openvpn-auth-okta-2.8.2/debian/libokta-auth-validator.installusr/lib/*/libokta-auth-validator.so 07070100000017000081A4000000000000000000000001663BA04300000043000000000000000000000000000000000000004200000000openvpn-auth-okta-2.8.2/debian/okta-auth-validator-common.installetc/okta-auth-validator/pinset.cfg etc/okta-auth-validator/api.ini 07070100000018000081A4000000000000000000000001663BA0430000001C000000000000000000000000000000000000003B00000000openvpn-auth-okta-2.8.2/debian/okta-auth-validator.installusr/bin/okta-auth-validator 07070100000019000081A4000000000000000000000001663BA04300000036000000000000000000000000000000000000003900000000openvpn-auth-okta-2.8.2/debian/openvpn-auth-okta.installusr/lib/*/openvpn/plugins/openvpn-plugin-auth-okta.so 0707010000001A000081ED000000000000000000000001663BA043000002B4000000000000000000000000000000000000002500000000openvpn-auth-okta-2.8.2/debian/rules#!/usr/bin/make -f # -*- makefile -*- # export DH_VERBOSE=1 USERNAME := $(shell echo $$USER) GOLDFLAGS := -ldflags '-extldflags "-static"' GOFLAGS := -buildmode=pie -mod=vendor -a $(GOLDFLAGS) DEB_HOST_MULTIARCH ?= $(shell dpkg-architecture -qDEB_HOST_MULTIARCH) %: dh $@ ifeq ($(USERNAME), abuild) override_dh_auto_configure: tar xf ../SOURCES/vendor.tar.gz override_dh_auto_build: make all override_dh_auto_test: endif override_dh_fixperms: dh_fixperms chmod 0644 debian/tmp/etc/okta-auth-validator/pinset.cfg chmod 0640 debian/tmp/etc/okta-auth-validator/api.ini override_dh_auto_install: make install DESTDIR=$(CURDIR)/debian/tmp LIB_PREFIX=/usr/lib/$(DEB_HOST_MULTIARCH) 0707010000001B000041ED000000000000000000000002663BA04300000000000000000000000000000000000000000000002600000000openvpn-auth-okta-2.8.2/debian/source0707010000001C000081A4000000000000000000000001663BA0430000000D000000000000000000000000000000000000002D00000000openvpn-auth-okta-2.8.2/debian/source/format3.0 (native) 0707010000001D000041ED000000000000000000000002663BA04300000000000000000000000000000000000000000000001D00000000openvpn-auth-okta-2.8.2/dist0707010000001E000081A4000000000000000000000001663BA04300000278000000000000000000000000000000000000003300000000openvpn-auth-okta-2.8.2/dist/openvpn-auth-okta.dscFormat: 3.0 (native) DEBTRANSFORM-RELEASE: 1 Source: openvpn-auth-okta Binary: openvpn-auth-okta Architecture: any Version: 2.8.2 DEBTRANSFORM-TAR: openvpn-auth-okta-2.8.2.tar.xz Maintainer: Foundation Squad <foundation@algolia.com> Homepage: https://github.com/algolia/openvpn-auth-okta Standards-Version: 4.5.10 Build-Depends: debhelper, make, gcc, golang-1.22 Package-List: openvpn-auth-okta deb base optional arch=any okta-auth-validator deb base optional arch=any libokta-auth-validator deb base optional arch=any libokta-auth-validator-dev deb base optional arch=all okta-auth-validator-common deb base optional arch=all 0707010000001F000081A4000000000000000000000001663BA0430000009B000000000000000000000000000000000000003900000000openvpn-auth-okta-2.8.2/dist/openvpn-auth-okta.rpmlintrcaddFilter("statically-linked-binary .*/usr/bin/okta-auth-validator") addFilter("shared-lib-without-dependency-information .*/usr/bin/okta-auth-validator") 07070100000020000081A4000000000000000000000001663BA0430000441C000000000000000000000000000000000000003400000000openvpn-auth-okta-2.8.2/dist/openvpn-auth-okta.specName: openvpn-auth-okta Version: 2.8.2 Release: 1%{?dist} Summary: Go programming language Group: Productivity/Networking/Security License: MPL-2.0 URL: https://github.com/algolia/openvpn-auth-okta Source0: %{name}-%{version}.tar.xz Source1: vendor.tar.gz Source99: %{name}.rpmlintrc BuildRequires: golang-1.22 BuildRequires: gcc BuildRequires: make Requires: libokta-auth-validator = %{version} Requires: okta-auth-validator-common = %{version} BuildRoot: %{_tmppath}/%{name}-%{version}-build %define plugin_dir %{_libdir}/openvpn/plugins %description This is a plugin for OpenVPN (Community Edition) that authenticates users directly against Okta, with support for MFA. %package -n okta-auth-validator Summary: Command line tool to authenticate against Okta Requires: okta-auth-validator-common = %{version} %description -n okta-auth-validator This is a command line tool that authenticates users directly against Okta, with support for MFA. %package -n libokta-auth-validator Summary: Shared library to authenticate against Okta Requires: okta-auth-validator-common = %{version} %description -n libokta-auth-validator Shared library that allows to authenticates user directly against Okta, with support for MFA. %package -n libokta-auth-validator-devel Summary: Development files for libokta-auth-validator Group: Development/Tools/Other Requires: libokta-auth-validator = %{version} %description -n libokta-auth-validator-devel Development files for libokta-auth-validator, a shared library that allows to authenticates user directly against Okta, with support for MFA. %package -n okta-auth-validator-common Summary: Config files for libokta-auth-validator %description -n okta-auth-validator-common Config files for openvpn-auth-okta, okta-auth-validator, libokta-auth-validator %prep %setup -q -n %{name}-%{version} tar xf ../../SOURCES/vendor.tar.gz %build make %install make DESTDIR=%{buildroot} LIB_PREFIX=%{_libdir} install %files %dir %{_libdir}/openvpn %dir %{plugin_dir}/ %attr(0644,root,root) %{plugin_dir}/openvpn-plugin-auth-okta.so %files -n okta-auth-validator-common %dir /etc/okta-auth-validator/ %attr(0644,root,root) %config(noreplace) /etc/okta-auth-validator/pinset.cfg %attr(0640,root,root) %config(noreplace) /etc/okta-auth-validator/api.ini %files -n okta-auth-validator %attr(0755,root,root) /usr/bin/okta-auth-validator %files -n libokta-auth-validator %attr(0644,root,root) %{_libdir}/libokta-auth-validator.so %files -n libokta-auth-validator-devel %attr(0644,root,root) %{_includedir}/libokta-auth-validator.h %changelog * Tue May 07 2024 Jeremy Jacque <jeremy.jacque@algolia.com> 2.8.2-1 - chore(deps): bump github.com/stretchr/testify from 1.8.4 to 1.9.0 - chore(deps): bump github.com/go-playground/validator/v10 - chore(deps): bump github.com/phuslu/log from 1.0.88 to 1.0.89 - chore(deps): bump github.com/phuslu/log from 1.0.89 to 1.0.90 - chore(deps): bump golang.org/x/net from 0.21.0 to 0.23.0 - chore(deps): bump github.com/phuslu/log from 1.0.90 to 1.0.91 - chore(deps): bump github.com/go-playground/validator/v10 - chore(deps): bump github.com/phuslu/log from 1.0.91 to 1.0.92 - chore(deps): bump github.com/phuslu/log from 1.0.92 to 1.0.93 - chore(deps): bump github.com/phuslu/log from 1.0.93 to 1.0.96 - chore(deps): bump github.com/phuslu/log from 1.0.96 to 1.0.97 - feat(lib): allow to use a struct for PluginEnv - feat(lib): provide a way to compute lib args from plugin envp, args passed as a struct - fix(Makefile): use proper inc dirs for gcc and cppcheck - feat(lib): rename the C function computing args - doc: add some comments - fix(lib): gotfmt - chore(lib): let the user allocate and free himself the ArgsOktaAuthValidatorV2 struct - refacto(plugin): mutualize error code during lib related calls - chore(cmd): add missing license header * Mon Feb 26 2024 Jeremy Jacque <jeremy.jacque@algolia.com> 2.8.1-1 - chore(deps): bump github.com/google/uuid from 1.5.0 to 1.6.0 - chore(deps): bump github.com/go-playground/validator/v10 - chore(ci): check go fmt - fix(cmd): gofmt * Sun Feb 25 2024 Jeremy Jacque <jeremy.jacque@algolia.com> 2.8.0-1 - chore(deps): bump golang.org/x/crypto from 0.7.0 to 0.17.0 - chore(deps): bump golang.org/x/net from 0.8.0 to 0.17.0 - fix(oktaApiAuth): do not return "valid" http 500 on request error - chore: add comments and basic traces - chore(oktaApiAuth): add validity checks on Okta groups - feat(validator): allow various log level and to set it up in the conf file - chore(doc): update coverage badge - chore: normalize validator constructor name - chore: normalize oktaApiAuth constructor name - chore(validator): if log level has been set in constructor, use it as default if not provided in config - refacto: use phuslu/log instead of logrus for better perf/memory usage - chore(go): clean sums * Thu Feb 22 2024 Jeremy Jacque <jeremy.jacque@algolia.com> 2.7.0-1 - chore(go): add go-playground/validator for API response validation - chore(go): use proper json unmarshaling/validation instead of string parsing for API responses - refacto(oktaApiAuth): factorize common (first step) MFA verification code - chore(oktaApiAuth/test): test MFA with multiple push retries - refacto(oktaApiAuth/test): split TestAuth function - chore(oktaApiAuth) respect API spec anf honor HTTP codes - chore(oktaApiAuth/test): respect API spec anf honor HTTP codes - chore(oktaApiAuth): factorize TOTP and Push MFA verification code - refacto(oktaApiAuth): dedup and simplify doAuthFirstStep code - fix(oktaApiAuth): Okta will never answer success at first push MFA verify call - fix(oktaApiAuth): status is mandatory in a AuthResponse - fix(oktaApiAuth): add missing cancel in verifyFactors, simplify if/else with return - fix(oktaApiAuth): return proper error for checkAllowedGroups HTTP error - chore(oktaApiAuth/test): cover all Auth scenarii - chore(oktaApiAuth/test): increase coverage of CheckAllowedGroups - chore(doc): update coverage badge - chore(oktaApiAuth/test): add auth_invalid_totp_no_sum fixture file - chore(oktaApiAuth): cancelAuth is a fire & forget, no need for return values - chore(oktaApiAuth): add some debug messages - chore(doc): update coverage badge - fix(oktaApiAuth): reset AuthResponse at each loop iteration to prevent persistency issues - chore: make TOTP to Push fallback configurable - refacto: simplify code - remove else when not needed * Fri Feb 09 2024 vagrant <jeremy.jacque@algolia.com> 2.6.1-1 - chore(go): fmt - chore(ci): remove snyk jobs as we are decommissioning it - chore(doc): simplify pkg install section by using OBS instruction page - chore: add license header to source files * Fri Dec 22 2023 Jeremy Jacque <jeremy.jacque@algolia.com> 2.6.0-1 - fix(plugin): add missing dlclose - chore(Makefile): minimize built file size and trimpath - chore(Makefile): clean unused vars and targets, tune go build params - refacto(go): simplify logging by using different fomatter before and after username is set - chore(doc): update coverage badge - chore(doc): add a section about logs - refacto(oktaApiAuth): split oktaApiAuth package into multiple files - refacto(oktaApiAuth/test): split oktaApiAuth tests into multiple files - refacto(validator): integrate utils pkg as it is only used by validator - refacto(validator): split validator package into multiple files - refacto(validator/test): split validator tests into multiple files - refacto(Makefile): use a shell cmd to get the full list of pkg files for dependencies - fix(Makefile): GOLDFLAGS has been renamed GOPLUGIN_LDFLAGS in a previous commit - refacto(oktaApiAuth/test): use types_test.go only for stuff common to multiple tests - refacto(validator): move checkControlFilePerm to utils(_test) - chore(oktaApiAuth): add a todo for Pool only used in tests * Tue Dec 19 2023 Jeremy Jacque <jeremy.jacque@algolia.com> 2.5.10-1 - chore(oktaApiAuth): rephrase/enrich log outputs - chore(validator): rephrase/enrich log outputs - chore: create a dedicated func for logging setup (formatting with uuid, log-level) - chore(go): add uuid package needed for logging - chore(tools): update url for https://toolkit.okta.com/apps/ * Mon Dec 18 2023 Jeremy Jacque <jeremy.jacque@algolia.com> 2.5.9-1 - refacto(oktaApiAuth): handle totp and push MFA sequentialy with different error msg - chore(oktaApiAuth/test): rename MFA errors, add test for invalid response for totp auth - fix(oktaApiAuth): continue outer loop when push triggers error and not last - chore(oktaApiAuth/test): test if multiple TOTP or push providers are possible - chore(doc): update coverage badge - refacto(oktaApiAuth): split validateUserMFA with 2 more functions (TOTP and push) - chore(oktaApiAuth/test): implement tests following refacto * Sat Dec 16 2023 Jeremy Jacque <jeremy.jacque@algolia.com> 2.5.8-1 - fix(oktaApiAuth): user may have multiple OTP providers, try all * Sat Dec 16 2023 Jeremy Jacque <jeremy.jacque@algolia.com> 2.5.7-1 - fix(oktaApiAuth): handle properly transaction cancelation, only when needed - fix(oktaApiAuth/test): fix with new calls to transaction cancellation - chore(doc): update coverage badge - chore(oktaApiAuth): homogenize / clean auth logs - fix(oktaApiAuth): handle properly TOTP error (invalid passcode) - chore(oktaApiAuth): simplify validateMFA signature - chore(oktaApiAuth): rename some functions/vars for better readability - fix(oktaApiAuth): respect MFAPushMaxRetries count, saves an API call/MFAPushDelaySeconds sleep - feat(oktaApiAuth): handle expired password (only when no active MFA) - chore(config): clean a bit api.ini config template - chore(doc): rearrange and fix typo in README - fix(dist): add missing packages in dsc * Fri Dec 15 2023 Jeremy Jacque <jeremy.jacque@algolia.com> 2.5.6-1 - style: gofmt some source files - refacto(oktaApiAuth): reduce gocyclo score of Auth to an acceptable score - chore(oktaApiAuth): homogenize some Auth logs - chore(oktaApiAuth/test): add a test for invalid preauth response - chore(doc): update coverage badge * Thu Dec 14 2023 Jeremy Jacque <jeremy.jacque@algolia.com> 2.5.5-1 - chore(validator): add msg for setup failures - chore: use logrus to have a clean output - fix(Makefile): missing source deps in targets - chore(doc): update coverage badge - fix(Makefile): allow to build on MacOS - chore(validator): handle properly new config files names - chore(Makefile): add proper MacOS ldflags for libs - chore(oktaApiAuth): allow POST and GET in oktaReq - feat(oktaApiAuth): check if user is a member of an AllowedGroup - feat((oktaApiAuth/test): test checkAllowedGroups functions - feat(config): add AllowedGroups option - chore(doc): update coverage badge - feat(oktaApiAuth/test): add invalid payload test - chore(validator/test): check config file detection - chore(validator/test): test wrongly formatted ini file - chore(doc): update coverage badge - chore(github): remove refs to pip in dependabot * Thu Oct 26 2023 Jeremy Jacque <jeremy.jacque@algolia.com> 2.5.4-1 - chore: rename config files - fix(doc): fix invalid license header - chore(doc): small README updates - chore(doc): remove CODE.md as the pkg is now referenced in pkg.go.dev - chore(doc): update authors - chore(dist): move tool to update packages version - chore(config): move config files to a dedicated dir - chore(Makefile): enforce some compiler options - fix(debian): ensure proper config files permissions * Wed Oct 25 2023 Jeremy Jacque <jeremy.jacque@algolia.com> 2.5.3-1 - style(utils): use gofmt std indentation - style(oktaApiAuth): use gofmt std indentation - style(validator): use gofmt std indentation - style(cmd): use gofmt std indentation - style(lib): use gofmt std indentation - chore(doc): update coverage badge - chore(doc): add doc ref and goreportcard badges - chore(pkg): rename config files - chore(Makefile): rename config files - chore(dist): rename config files - chore(doc): rename config files - chore(git): rename config files * Tue Oct 24 2023 Jeremy Jacque <jeremy.jacque@algolia.com> 2.5.2-1 - chore(oktaApiAuth): add comments for functions - chore(utils): add comments for functions - chore(validator): remove useless function - chore(validator): add comments for functions - chore(validator): do not set clientIp when not needed - chore(oktaApiAuth): clientIp is not set anymore to 0.0.0.0 - chore(oktaApiAuth): add struct comments - chore(validator): add struct comments * Tue Oct 24 2023 Jeremy Jacque <jeremy.jacque@algolia.com> 2.5.1-1 - chore(pkg): relocate http pool initialisation from utils to oktaApiAuth - chore(utils): simplify test - chore(utils): add a function to remove comments from a slice - chore(validator): remove from the pinset list empty lines and comments - chore(utils): simplify CheckUsernameFormat * Sun Oct 22 2023 Jeremy Jacque <jeremy.jacque@algolia.com> 2.5.0-1 - chore(pkg): remove now useless types package - chore(oktaApiAuth): move types in pkg, split constructor, make needed struct fields public - chore(oktaApiAuth/test): struct has been renamed, test InitPool - chore(validator): move types in pkg, parse for passcode here, move okta api related struct - chore(validator/test): adapt to latest changes, add more tests - chore(lib): adapt to struct moves - chore(testing): add new fixture for validator - chore(doc): reflect latest changes - chore(validator): remove useles comments, error test - chore(doc): update coverage badge after new tests implem - chore(debian): add missing common package for config files - chore(dist): split RPM pkg to have a dedicated config file one - chore(tools): small fix - fix(dist): add missing description for okta-auth-validator-common - chore(dist): fix some rpmlint issues - chore(cfg): remove algolia ref * Fri Oct 20 2023 Jeremy Jacque <jeremy.jacque@algolia.com> 2.4.3-1 - chore: change plugin file name to be aligned with other OpenVPN plugins - chore(doc): fix typo in install section and add info about OpenVPN tmp dir - chore(plugin): rename source according to new plugin file name - chore(doc): update with new plugin name - chore(git): now all produced files are in a dedicated dir, so ignore this dir * Fri Oct 20 2023 Jeremy Jacque <jeremy.jacque@algolia.com> 2.4.2-1 - chore(validator): refacto to remove useless functions and better err control on Authenticate - chore(cmd/okta-auth-validator): adapt to new Authenticate return type - chore(lib): adapt to new Authenticate return type - chore(validator/test): test new Authenticate err returned - chore(oktaApiAuth/test): test the case where we need to sort the offered MFAs - fix(validator/test): handle possible nil error - chore(oktaApiAuth/test): test the case where preAuth fails (connection issue) - chore(fixtures): add preauth_mfa_required_multi used by oktaApiAuth/TestAuth - chore(doc): update coverage badge after new tests implem * Thu Oct 19 2023 Jeremy Jacque <jeremy.jacque@algolia.com> 2.4.1-1 - chore: renamed repo and packages - chore(github): add code owners and PR template - chore(Makefile): change config files location - chore(doc): change config files location - chore(dist): change config files location - chore(validator): change default config files location * Thu Oct 19 2023 Jeremy Jacque <jeremy.jacque@algolia.com> 2.4.0-1 - chore(debian): split package - chore(dist): split package - chore(dist): update rpmlintrc filters - fix(tools): work on current branch to generate changelogs * Wed Oct 18 2023 Jeremy Jacque <jeremy.jacque@algolia.com> 2.3.4-1 - chore(tools): fix distro name in changelog creation - fix(Makefile): wrong soname for golang c-shared lib - chore: remove unneeded defer_simple source code - chore(doc): add section about package installation - chore(dist): remove refs to defer_simple in spec file - chore(dist): bump version * Wed Oct 18 2023 Jeremy Jacque <jeremy.jacque@algolia.com> 2.3.3-1 - chore(Makefile): refacto to handle properly dlopened libokta-openvpn.so - chore(debian): adapt rules after Makefile refacto - fix(defer_okta_openvpn): context is needed, dlopen is mandatory to respect signals - fix(defer_simple): passing wrong args array to deferred_auth_handler - chore(pkg): use a struct instead of env to hold shared lib setup - chore(cmd/lib): use new validator.Setup params - chore(doc): update coverage badge - chore(debian): update version - chore(Makefile): symlink is not needed as dlopen follows ld.so - chore(Makefile): add missing soname to shared lib - chore(Makefile): binary should bo to /usr/bin - chore(dist): add files needed to build packages - chore(dist): add script to manage versions - chore(debian): Update changelog for 2.3.2 release - chore(dist): Update changelog for 2.3.2 release - chore(debian): add source format - fix(Makefile): create bin dir before install - fix(debian): wrong package format - chore(defer_okta_openvpn): fix indent, do not store useless pointer in context - chore: rename function for clarity - chore(Makefile): DESTDIR should be empty by default - chore: rename okta_openvpn to okta-auth-validator to be more generic - fix(lib): wrong (previous) export function name - chore: rename C source file - chore: rename lib to be more generic - chore(oktaApiAuth): do not flood openvpn logs with useless messages * Wed Oct 18 2023 Jeremy Jacque <jeremy.jacque@algolia.com> 2.3.2-1 - chore(Makefile): add missing soname to shared lib - chore(Makefile): binary should bo to /usr/bin - chore(dist): add files needed to build packages - chore(dist): add script to manage versions 07070100000021000081A4000000000000000000000001663BA0430000044B000000000000000000000000000000000000002F00000000openvpn-auth-okta-2.8.2/dist/update_version.sh#!/bin/bash # Update all dist, debian files when bumping version # # Needs https://github.com/agx/git-buildpackage version=$1 branch=$(git rev-parse --abbrev-ref HEAD) git tag -f -a "v${version}" -m "v${version}" sed -i'' -e "s/^\(Version: \).*/\1${version}/" dist/openvpn-auth-okta.spec gbp rpm-ch --packaging-branch="${branch}" \ --packaging-tag="v%(version)s" \ --spec-file=dist/openvpn-auth-okta.spec \ --git-author \ --spawn-editor=no git add dist/openvpn-auth-okta.spec git commit -m "chore(dist): Update changelog for ${version} release" sed -i'' -e "s/^\(DEBTRANSFORM-TAR: openvpn-auth-okta-\).*\(\.tar\.xz\)$/\1${version}\2/" dist/openvpn-auth-okta.dsc sed -i'' -e "s/^\(Version: \).*/\1${version}/" dist/openvpn-auth-okta.dsc git add dist/openvpn-auth-okta.dsc gbp dch --debian-branch="${branch}" \ -c --commit-msg="chore(debian): Update changelog for %(version)s release" \ --release \ --git-author \ --distribution=stable --force-distribution \ --spawn-editor=no \ --debian-tag="v%(version)s" \ -N "${version}" git tag -f -a "v${version}" -m "v${version}" 07070100000022000081A4000000000000000000000001663BA0430000036F000000000000000000000000000000000000001F00000000openvpn-auth-okta-2.8.2/go.modmodule gopkg.in/algolia/openvpn-auth-okta.v2 go 1.21 require ( github.com/go-playground/validator/v10 v10.20.0 github.com/google/uuid v1.6.0 github.com/phuslu/log v1.0.97 gopkg.in/h2non/gock.v1 v1.1.2 gopkg.in/ini.v1 v1.67.0 ) require ( github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect github.com/leodido/go-urn v1.4.0 // indirect golang.org/x/crypto v0.21.0 // indirect golang.org/x/net v0.23.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/testify v1.9.0 gopkg.in/yaml.v3 v3.0.1 // indirect ) 07070100000023000081A4000000000000000000000001663BA04300000E7D000000000000000000000000000000000000001F00000000openvpn-auth-okta-2.8.2/go.sumgithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 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/phuslu/log v1.0.97 h1:J3rI0BPDHjmyvgO0/XFMgn9vq03Liftr7hjWyVLdX5w= github.com/phuslu/log v1.0.97/go.mod h1:F8osGJADo5qLK/0F88djWwdyoZZ9xDJQL1HYRHFEkS0= 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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= 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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 07070100000024000041ED000000000000000000000002663BA04300000000000000000000000000000000000000000000001C00000000openvpn-auth-okta-2.8.2/lib07070100000025000081A4000000000000000000000001663BA04300000DBD000000000000000000000000000000000000003600000000openvpn-auth-okta-2.8.2/lib/libokta-auth-validator.go// SPDX-FileCopyrightText: 2023-Present Algolia // // SPDX-License-Identifier: MPL-2.0 // // Copyright 2023-Present Algolia // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. /* This lib is meant to be used along with OpenVPN: it's purpose is to be dynamically loaded (using dlopen/dlsyms/...) by a C "plugin wrapper". The following C functions are exported (and interesting): - ArgsOktaAuthValidatorV2 * oav_args_from_env_v2(const char *envp[]) that creates an plugin argument dedicated struct from the OPENVPN_PLUGIN env - extern void OktaAuthValidatorV2(ArgsOktaAuthValidatorV2* args) that run the Go OktaAuthValidator authentication (using the previously created struct) */ package main import ( "gopkg.in/algolia/openvpn-auth-okta.v2/pkg/validator" ) /* #ifndef _OKTA_AUTH_VALIDATOR_ #define _OKTA_AUTH_VALIDATOR_ #include <stdbool.h> #include <stdlib.h> #include <string.h> // Given an environmental variable name, search // the envp array for its value, returning it // if found or an empty string otherwise. // From https://github.com/OpenVPN/openvpn/blob/master/sample/sample-plugins/log/log_v3.c static const char * get_env(const char *name, const char *envp[]) { if (envp) { int i; const int namelen = strlen(name); for (i = 0; envp[i]; ++i) { if (!strncmp(envp[i], name, namelen)) { const char *cp = envp[i] + namelen; if (*cp == '=') { return cp + 1; } } } } // Return an empty string here (as expected by the Golang c-shared lib) return ""; } // Used to pass arguments to OktaAuthValidatorV2() // None of this should be null, an empty string is at least expected typedef struct { const char *CtrFile; const char *IP; const char *CN; const char *User; const char *Pass; } ArgsOktaAuthValidatorV2; // Extract from envp all what's needed to populate a struct suitable // for OktaAuthValidatorV2 // The go_args pointer has to be allocated static bool oav_args_from_env_v2(const char *envp[], ArgsOktaAuthValidatorV2 *go_args) { if(go_args) { go_args->CtrFile = get_env("auth_control_file", envp); go_args->IP = get_env("untrusted_ip", envp); go_args->CN = get_env("common_name", envp); go_args->User = get_env("username", envp); go_args->Pass = get_env("password", envp); return true; } return false; } #endif */ import "C" type PluginEnv = validator.PluginEnv //export OktaAuthValidatorV2 func OktaAuthValidatorV2(args *C.ArgsOktaAuthValidatorV2) { pluginEnv := &PluginEnv{ Username: C.GoString(args.User), CommonName: C.GoString(args.CN), Password: C.GoString(args.Pass), ClientIp: C.GoString(args.IP), ControlFile: C.GoString(args.CtrFile), } v := validator.New() if res := v.Setup(true, nil, pluginEnv); !res { return } _ = v.Authenticate() v.WriteControlFile() } // Deprecated: replaced by OktaAuthValidatorV2 // //export OktaAuthValidator func OktaAuthValidator(ctrF *C.char, ip *C.char, cn *C.char, user *C.char, pass *C.char) { pluginEnv := &PluginEnv{ Username: C.GoString(user), CommonName: C.GoString(cn), Password: C.GoString(pass), ClientIp: C.GoString(ip), ControlFile: C.GoString(ctrF), } v := validator.New() if res := v.Setup(true, nil, pluginEnv); !res { return } _ = v.Authenticate() v.WriteControlFile() } func main() { } 07070100000026000081A4000000000000000000000001663BA0430000010B000000000000000000000000000000000000002200000000openvpn-auth-okta-2.8.2/okta.json{ "name": "openvpn-auth-okta", "description": "A Golang implementation of the Okta OpenVPN plugin.", "url": "https://gopkg.in/algolia/openvpn-auth-okta.v2", "categories": [ "authentication", "authorization", "mfa", "integration-network" ] } 07070100000027000081A4000000000000000000000001663BA04300001C1C000000000000000000000000000000000000003300000000openvpn-auth-okta-2.8.2/openvpn-plugin-auth-okta.c//go:build ignore /* * OpenVPN -- An application to securely tunnel IP networks * over a single TCP/UDP port, with support for SSL/TLS-based * session authentication and key exchange, * packet encryption, packet authentication, and * packet compression. * * Copyright (C) 2002-2018 OpenVPN Inc <sales@openvpn.net> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <signal.h> #include <dlfcn.h> #include <openvpn-plugin.h> #include <libokta-auth-validator.h> /* Pointers to functions exported from openvpn */ static plugin_log_t plugin_log = NULL; /* * Constants indicating minimum API and struct versions by the functions * in this plugin. Consult openvpn-plugin.h, look for: * OPENVPN_PLUGIN_VERSION and OPENVPN_PLUGINv3_STRUCTVER * * Strictly speaking, this sample code only requires plugin_log, a feature * of structver version 1. However, '1' lines up with ancient versions * of openvpn that are past end-of-support. As such, we are requiring * structver '5' here to indicate a desire for modern openvpn, rather * than a need for any particular feature found in structver beyond '1'. */ #define OPENVPN_PLUGIN_VERSION_MIN 3 #define OPENVPN_PLUGIN_STRUCTVER_MIN 5 /* * Our context, where we keep our state. */ struct plugin_context {}; /* module name for plugin_log() */ static char *MODULE = "openvpn-plugin-auth-okta"; void handle_sigchld(int sig) { /* * nonblocking wait (WNOHANG) for any child (-1) to come back */ while(waitpid((pid_t)(-1), 0, WNOHANG) > 0) {} } /* Require a minimum OpenVPN Plugin API */ OPENVPN_EXPORT int openvpn_plugin_min_version_required_v1() { return OPENVPN_PLUGIN_VERSION_MIN; } /* use v3 functions so we can use openvpn's logging and base64 etc. */ OPENVPN_EXPORT int openvpn_plugin_open_v3(const int v3structver, struct openvpn_plugin_args_open_in const *args, struct openvpn_plugin_args_open_return *ret) { struct plugin_context *context; if (v3structver < OPENVPN_PLUGIN_STRUCTVER_MIN) { fprintf(stderr, "%s: this plugin is incompatible with the running version of OpenVPN\n", MODULE); return OPENVPN_PLUGIN_FUNC_ERROR; } /* Save global pointers to functions exported from openvpn */ plugin_log = args->callbacks->plugin_log; /* * Allocate our context */ context = (struct plugin_context *) calloc(1, sizeof(struct plugin_context)); if (!context) { goto error; } /* * Which callbacks to intercept. */ ret->type_mask = OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY); ret->handle = (openvpn_plugin_handle_t *) context; return OPENVPN_PLUGIN_FUNC_SUCCESS; error: if (context) { free(context); } plugin_log(PLOG_NOTE, MODULE, "initialization failed"); return OPENVPN_PLUGIN_FUNC_ERROR; } static int deferred_auth_handler(const char *argv[], const char *envp[]) { struct sigaction sa; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; sa.sa_handler = &handle_sigchld; if (sigaction(SIGCHLD, &sa, NULL) == -1) { return OPENVPN_PLUGIN_FUNC_ERROR; } /* we do not want to complicate our lives with having to wait() * for child processes (so they are not zombiefied) *and* we MUST NOT * fiddle with signal handlers (= shared with openvpn main), so * we use double-fork() trick. */ /* fork, sleep, succeed (no "real" auth done = always succeed) */ pid_t p1 = fork(); if (p1 < 0) /* Fork failed */ { return OPENVPN_PLUGIN_FUNC_ERROR; } if (p1 > 0) /* parent process */ { waitpid(p1, NULL, 0); return OPENVPN_PLUGIN_FUNC_DEFERRED; } /* first gen child process, fork() again and exit() right away */ pid_t p2 = fork(); if (p2 < 0) { plugin_log(PLOG_ERR|PLOG_ERRNO, MODULE, "BACKGROUND: fork(2) failed"); exit(1); } if (p2 != 0) /* new parent: exit right away */ { exit(0); } /* (grand-)child process * - return status is communicated by file */ /* do mighty complicated work that will really take time here... */ void *handle; char *error; char *err_msg = NULL; // Load the Golang c-shared lib // dlopen is needed here, otherwise Go runtime wont respect alredy set signal handlers handle = dlopen ("libokta-auth-validator.so", RTLD_LAZY); if (!handle) { err_msg = "Can not load libopenvpn-auth-okta.so"; goto clean_exit; } // Clear any existing error dlerror(); void (*OktaAuthValidator_V2)(ArgsOktaAuthValidatorV2*) = dlsym(handle, "OktaAuthValidatorV2"); if ((error = dlerror()) != NULL) { err_msg = "Error loading OktaAuthValidatorV2 symbol from lib"; goto clean_exit; } // Allocate the struct needed to store OktaAuthValidatorV2 args ArgsOktaAuthValidatorV2* go_args = (ArgsOktaAuthValidatorV2 *) calloc(1, sizeof(ArgsOktaAuthValidatorV2)); if(!go_args) { err_msg = "Error allocating ArgsOktaAuthValidatorV2 struct"; goto clean_exit; } // Fill an ArgsOktaAuthValidatorV2 struct from the plugin env if (!oav_args_from_env_v2(envp, go_args)) { err_msg = "Error parsing plugin env with oav_args_from_env_v2"; goto clean_exit; } plugin_log(PLOG_DEBUG, MODULE, "Initialization of the OktaAuthValidator lib succeeded"); // Call the Golang c-shared lib function (*OktaAuthValidator_V2)(go_args); clean_exit: if (handle) { dlclose(handle); } if(go_args) { free(go_args); } if(err_msg != NULL) { plugin_log(PLOG_ERR, MODULE, "%s", err_msg); exit(127); } exit(0); } OPENVPN_EXPORT int openvpn_plugin_func_v3(const int v3structver, struct openvpn_plugin_args_func_in const *args, struct openvpn_plugin_args_func_return *ret) { if (v3structver < OPENVPN_PLUGIN_STRUCTVER_MIN) { fprintf(stderr, "%s: this plugin is incompatible with the running version of OpenVPN\n", MODULE); return OPENVPN_PLUGIN_FUNC_ERROR; } const char **argv = args->argv; const char **envp = args->envp; struct plugin_context *context = (struct plugin_context *) args->handle; switch (args->type) { case OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY: return deferred_auth_handler(argv, envp); default: plugin_log(PLOG_NOTE, MODULE, "OPENVPN_PLUGIN_?"); return OPENVPN_PLUGIN_FUNC_ERROR; } } OPENVPN_EXPORT void openvpn_plugin_close_v1(openvpn_plugin_handle_t handle) { struct plugin_context *context = (struct plugin_context *) handle; free(context); } 07070100000028000081A4000000000000000000000001663BA0430000776E000000000000000000000000000000000000002900000000openvpn-auth-okta-2.8.2/openvpn-plugin.h/* include/openvpn-plugin.h. Generated from openvpn-plugin.h.in by configure. */ /* * OpenVPN -- An application to securely tunnel IP networks * over a single TCP/UDP port, with support for SSL/TLS-based * session authentication and key exchange, * packet encryption, packet authentication, and * packet compression. * * Copyright (C) 2002-2018 OpenVPN Inc <sales@openvpn.net> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #ifndef OPENVPN_PLUGIN_H_ #define OPENVPN_PLUGIN_H_ #define OPENVPN_PLUGIN_VERSION 3 #ifdef ENABLE_CRYPTO #ifdef ENABLE_CRYPTO_MBEDTLS #include <mbedtls/x509_crt.h> #ifndef __OPENVPN_X509_CERT_T_DECLARED #define __OPENVPN_X509_CERT_T_DECLARED typedef mbedtls_x509_crt openvpn_x509_cert_t; #endif #else /* ifdef ENABLE_CRYPTO_MBEDTLS */ #include <openssl/x509.h> #ifndef __OPENVPN_X509_CERT_T_DECLARED #define __OPENVPN_X509_CERT_T_DECLARED typedef X509 openvpn_x509_cert_t; #endif #endif #endif #include <stdarg.h> #include <stddef.h> #ifdef __cplusplus extern "C" { #endif /* Provide some basic version information to plug-ins at OpenVPN compile time * This is will not be the complete version */ #define OPENVPN_VERSION_MAJOR 2 #define OPENVPN_VERSION_MINOR 4 #define OPENVPN_VERSION_PATCH ".11" /* * Plug-in types. These types correspond to the set of script callbacks * supported by OpenVPN. * * This is the general call sequence to expect when running in server mode: * * Initial Server Startup: * * FUNC: openvpn_plugin_open_v1 * FUNC: openvpn_plugin_client_constructor_v1 (this is the top-level "generic" * client template) * FUNC: openvpn_plugin_func_v1 OPENVPN_PLUGIN_UP * FUNC: openvpn_plugin_func_v1 OPENVPN_PLUGIN_ROUTE_UP * * New Client Connection: * * FUNC: openvpn_plugin_client_constructor_v1 * FUNC: openvpn_plugin_func_v1 OPENVPN_PLUGIN_ENABLE_PF * FUNC: openvpn_plugin_func_v1 OPENVPN_PLUGIN_TLS_VERIFY (called once for every cert * in the server chain) * FUNC: openvpn_plugin_func_v1 OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY * FUNC: openvpn_plugin_func_v1 OPENVPN_PLUGIN_TLS_FINAL * FUNC: openvpn_plugin_func_v1 OPENVPN_PLUGIN_IPCHANGE * * [If OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY returned OPENVPN_PLUGIN_FUNC_DEFERRED, * we don't proceed until authentication is verified via auth_control_file] * * FUNC: openvpn_plugin_func_v1 OPENVPN_PLUGIN_CLIENT_CONNECT_V2 * FUNC: openvpn_plugin_func_v1 OPENVPN_PLUGIN_LEARN_ADDRESS * * [Client session ensues] * * For each "TLS soft reset", according to reneg-sec option (or similar): * * FUNC: openvpn_plugin_func_v1 OPENVPN_PLUGIN_ENABLE_PF * * FUNC: openvpn_plugin_func_v1 OPENVPN_PLUGIN_TLS_VERIFY (called once for every cert * in the server chain) * FUNC: openvpn_plugin_func_v1 OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY * FUNC: openvpn_plugin_func_v1 OPENVPN_PLUGIN_TLS_FINAL * * [If OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY returned OPENVPN_PLUGIN_FUNC_DEFERRED, * we expect that authentication is verified via auth_control_file within * the number of seconds defined by the "hand-window" option. Data channel traffic * will continue to flow uninterrupted during this period.] * * [Client session continues] * * FUNC: openvpn_plugin_func_v1 OPENVPN_PLUGIN_CLIENT_DISCONNECT * FUNC: openvpn_plugin_client_destructor_v1 * * [ some time may pass ] * * FUNC: openvpn_plugin_func_v1 OPENVPN_PLUGIN_LEARN_ADDRESS (this coincides with a * lazy free of initial * learned addr object) * Server Shutdown: * * FUNC: openvpn_plugin_func_v1 OPENVPN_PLUGIN_DOWN * FUNC: openvpn_plugin_client_destructor_v1 (top-level "generic" client) * FUNC: openvpn_plugin_close_v1 */ #define OPENVPN_PLUGIN_UP 0 #define OPENVPN_PLUGIN_DOWN 1 #define OPENVPN_PLUGIN_ROUTE_UP 2 #define OPENVPN_PLUGIN_IPCHANGE 3 #define OPENVPN_PLUGIN_TLS_VERIFY 4 #define OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY 5 #define OPENVPN_PLUGIN_CLIENT_CONNECT 6 #define OPENVPN_PLUGIN_CLIENT_DISCONNECT 7 #define OPENVPN_PLUGIN_LEARN_ADDRESS 8 #define OPENVPN_PLUGIN_CLIENT_CONNECT_V2 9 #define OPENVPN_PLUGIN_TLS_FINAL 10 #define OPENVPN_PLUGIN_ENABLE_PF 11 #define OPENVPN_PLUGIN_ROUTE_PREDOWN 12 #define OPENVPN_PLUGIN_N 13 /* * Build a mask out of a set of plug-in types. */ #define OPENVPN_PLUGIN_MASK(x) (1<<(x)) /* * A pointer to a plugin-defined object which contains * the object state. */ typedef void *openvpn_plugin_handle_t; /* * Return value for openvpn_plugin_func_v1 function */ #define OPENVPN_PLUGIN_FUNC_SUCCESS 0 #define OPENVPN_PLUGIN_FUNC_ERROR 1 #define OPENVPN_PLUGIN_FUNC_DEFERRED 2 /* * For Windows (needs to be modified for MSVC) */ #if defined(_WIN32) && !defined(OPENVPN_PLUGIN_H) #define OPENVPN_EXPORT __declspec(dllexport) #else #define OPENVPN_EXPORT #endif /* * If OPENVPN_PLUGIN_H is defined, we know that we are being * included in an OpenVPN compile, rather than a plugin compile. */ #ifdef OPENVPN_PLUGIN_H /* * We are compiling OpenVPN. */ #define OPENVPN_PLUGIN_DEF typedef #define OPENVPN_PLUGIN_FUNC(name) (*name) #else /* ifdef OPENVPN_PLUGIN_H */ /* * We are compiling plugin. */ #define OPENVPN_PLUGIN_DEF OPENVPN_EXPORT #define OPENVPN_PLUGIN_FUNC(name) name #endif /* * Used by openvpn_plugin_func to return structured * data. The plugin should allocate all structure * instances, name strings, and value strings with * malloc, since OpenVPN will assume that it * can free the list by calling free() over the same. */ struct openvpn_plugin_string_list { struct openvpn_plugin_string_list *next; char *name; char *value; }; /* openvpn_plugin_{open,func}_v3() related structs */ /** * Defines version of the v3 plugin argument structs * * Whenever one or more of these structs are modified, this constant * must be updated. A changelog should be appended in this comment * as well, to make it easier to see what information is available * in the different versions. * * Version Comment * 1 Initial plugin v3 structures providing the same API as * the v2 plugin interface, X509 certificate information + * a logging API for plug-ins. * * 2 Added ssl_api member in struct openvpn_plugin_args_open_in * which identifies the SSL implementation OpenVPN is compiled * against. * * 3 Added ovpn_version, ovpn_version_major, ovpn_version_minor * and ovpn_version_patch to provide the runtime version of * OpenVPN to plug-ins. * * 4 Exported secure_memzero() as plugin_secure_memzero() * * 5 Exported openvpn_base64_encode() as plugin_base64_encode() * Exported openvpn_base64_decode() as plugin_base64_decode() */ #define OPENVPN_PLUGINv3_STRUCTVER 5 /** * Definitions needed for the plug-in callback functions. */ typedef enum { PLOG_ERR = (1 << 0),/* Error condition message */ PLOG_WARN = (1 << 1),/* General warning message */ PLOG_NOTE = (1 << 2),/* Informational message */ PLOG_DEBUG = (1 << 3),/* Debug message, displayed if verb >= 7 */ PLOG_ERRNO = (1 << 8),/* Add error description to message */ PLOG_NOMUTE = (1 << 9), /* Mute setting does not apply for message */ } openvpn_plugin_log_flags_t; #ifdef __GNUC__ #if __USE_MINGW_ANSI_STDIO #define _ovpn_chk_fmt(a, b) __attribute__ ((format(gnu_printf, (a), (b)))) #else #define _ovpn_chk_fmt(a, b) __attribute__ ((format(__printf__, (a), (b)))) #endif #else /* ifdef __GNUC__ */ #define _ovpn_chk_fmt(a, b) #endif typedef void (*plugin_log_t)(openvpn_plugin_log_flags_t flags, const char *plugin_name, const char *format, ...) _ovpn_chk_fmt (3, 4); typedef void (*plugin_vlog_t)(openvpn_plugin_log_flags_t flags, const char *plugin_name, const char *format, va_list arglist) _ovpn_chk_fmt (3, 0); /* #undef _ovpn_chk_fmt */ /** * Export of secure_memzero() to be used inside plug-ins * * @param data Pointer to data to zeroise * @param len Length of data, in bytes * */ typedef void (*plugin_secure_memzero_t)(void *data, size_t len); /** * Export of openvpn_base64_encode() to be used inside plug-ins * * @param data Pointer to data to BASE64 encode * @param size Length of data, in bytes * @param *str Pointer to the return buffer. This needed memory is * allocated by openvpn_base64_encode() and needs to be free()d * after use. * * @return int Returns the length of the buffer created, or -1 on error. * */ typedef int (*plugin_base64_encode_t)(const void *data, int size, char **str); /** * Export of openvpn_base64_decode() to be used inside plug-ins * * @param str Pointer to the BASE64 encoded data * @param data Pointer to the buffer where save the decoded data * @param size Size of the destination buffer * * @return int Returns the length of the decoded data, or -1 on error or * if the destination buffer is too small. * */ typedef int (*plugin_base64_decode_t)(const char *str, void *data, int size); /** * Used by the openvpn_plugin_open_v3() function to pass callback * function pointers to the plug-in. * * plugin_log * plugin_vlog : Use these functions to add information to the OpenVPN log file. * Messages will only be displayed if the plugin_name parameter * is set. PLOG_DEBUG messages will only be displayed with plug-in * debug log verbosity (at the time of writing that's verb >= 7). * * plugin_secure_memzero * : Use this function to securely wipe sensitive information from * memory. This function is declared in a way that the compiler * will not remove these function calls during the compiler * optimization phase. */ struct openvpn_plugin_callbacks { plugin_log_t plugin_log; plugin_vlog_t plugin_vlog; plugin_secure_memzero_t plugin_secure_memzero; plugin_base64_encode_t plugin_base64_encode; plugin_base64_decode_t plugin_base64_decode; }; /** * Used by the openvpn_plugin_open_v3() function to indicate to the * plug-in what kind of SSL implementation OpenVPN uses. This is * to avoid SEGV issues when OpenVPN is complied against mbed TLS * and the plug-in against OpenSSL. */ typedef enum { SSLAPI_NONE, SSLAPI_OPENSSL, SSLAPI_MBEDTLS } ovpnSSLAPI; /** * Arguments used to transport variables to the plug-in. * The struct openvpn_plugin_args_open_in is only used * by the openvpn_plugin_open_v3() function. * * STRUCT MEMBERS * * type_mask : Set by OpenVPN to the logical OR of all script * types which this version of OpenVPN supports. * * argv : a NULL-terminated array of options provided to the OpenVPN * "plug-in" directive. argv[0] is the dynamic library pathname. * * envp : a NULL-terminated array of OpenVPN-set environmental * variables in "name=value" format. Note that for security reasons, * these variables are not actually written to the "official" * environmental variable store of the process. * * callbacks : a pointer to the plug-in callback function struct. * */ struct openvpn_plugin_args_open_in { const int type_mask; const char **const argv; const char **const envp; struct openvpn_plugin_callbacks *callbacks; const ovpnSSLAPI ssl_api; const char *ovpn_version; const unsigned int ovpn_version_major; const unsigned int ovpn_version_minor; const char *const ovpn_version_patch; }; /** * Arguments used to transport variables from the plug-in back * to the OpenVPN process. The struct openvpn_plugin_args_open_return * is only used by the openvpn_plugin_open_v3() function. * * STRUCT MEMBERS * * type_mask : The plug-in should set this value to the logical OR of all script * types which the plug-in wants to intercept. For example, if the * script wants to intercept the client-connect and client-disconnect * script types: * * type_mask = OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_CLIENT_CONNECT) * | OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_CLIENT_DISCONNECT) * * handle : Pointer to a global plug-in context, created by the plug-in. This pointer * is passed on to the other plug-in calls. * * return_list : used to return data back to OpenVPN. * */ struct openvpn_plugin_args_open_return { int type_mask; openvpn_plugin_handle_t handle; struct openvpn_plugin_string_list **return_list; }; /** * Arguments used to transport variables to and from the * plug-in. The struct openvpn_plugin_args_func is only used * by the openvpn_plugin_func_v3() function. * * STRUCT MEMBERS: * * type : one of the PLUGIN_x types. * * argv : a NULL-terminated array of "command line" options which * would normally be passed to the script. argv[0] is the dynamic * library pathname. * * envp : a NULL-terminated array of OpenVPN-set environmental * variables in "name=value" format. Note that for security reasons, * these variables are not actually written to the "official" * environmental variable store of the process. * * handle : Pointer to a global plug-in context, created by the plug-in's openvpn_plugin_open_v3(). * * per_client_context : the per-client context pointer which was returned by * openvpn_plugin_client_constructor_v1, if defined. * * current_cert_depth : Certificate depth of the certificate being passed over (only if compiled with ENABLE_CRYPTO defined) * * *current_cert : X509 Certificate object received from the client (only if compiled with ENABLE_CRYPTO defined) * */ struct openvpn_plugin_args_func_in { const int type; const char **const argv; const char **const envp; openvpn_plugin_handle_t handle; void *per_client_context; #ifdef ENABLE_CRYPTO int current_cert_depth; openvpn_x509_cert_t *current_cert; #else int __current_cert_depth_disabled; /* Unused, for compatibility purposes only */ void *__current_cert_disabled; /* Unused, for compatibility purposes only */ #endif }; /** * Arguments used to transport variables to and from the * plug-in. The struct openvpn_plugin_args_func is only used * by the openvpn_plugin_func_v3() function. * * STRUCT MEMBERS: * * return_list : used to return data back to OpenVPN for further processing/usage by * the OpenVPN executable. * */ struct openvpn_plugin_args_func_return { struct openvpn_plugin_string_list **return_list; }; /* * Multiple plugin modules can be cascaded, and modules can be * used in tandem with scripts. The order of operation is that * the module func() functions are called in the order that * the modules were specified in the config file. If a script * was specified as well, it will be called last. If the * return code of the module/script controls an authentication * function (such as tls-verify or auth-user-pass-verify), then * every module and script must return success (0) in order for * the connection to be authenticated. * * Notes: * * Plugins which use a privilege-separation model (by forking in * their initialization function before the main OpenVPN process * downgrades root privileges and/or executes a chroot) must * daemonize after a fork if the "daemon" environmental variable is * set. In addition, if the "daemon_log_redirect" variable is set, * the plugin should preserve stdout/stderr across the daemon() * syscall. See the daemonize() function in plugin/auth-pam/auth-pam.c * for an example. */ /* * Prototypes for functions which OpenVPN plug-ins must define. */ /* * FUNCTION: openvpn_plugin_open_v2 * * REQUIRED: YES * * Called on initial plug-in load. OpenVPN will preserve plug-in state * across SIGUSR1 restarts but not across SIGHUP restarts. A SIGHUP reset * will cause the plugin to be closed and reopened. * * ARGUMENTS * * *type_mask : Set by OpenVPN to the logical OR of all script * types which this version of OpenVPN supports. The plug-in * should set this value to the logical OR of all script types * which the plug-in wants to intercept. For example, if the * script wants to intercept the client-connect and * client-disconnect script types: * * *type_mask = OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_CLIENT_CONNECT) * | OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_CLIENT_DISCONNECT) * * argv : a NULL-terminated array of options provided to the OpenVPN * "plug-in" directive. argv[0] is the dynamic library pathname. * * envp : a NULL-terminated array of OpenVPN-set environmental * variables in "name=value" format. Note that for security reasons, * these variables are not actually written to the "official" * environmental variable store of the process. * * return_list : used to return data back to OpenVPN. * * RETURN VALUE * * An openvpn_plugin_handle_t value on success, NULL on failure */ OPENVPN_PLUGIN_DEF openvpn_plugin_handle_t OPENVPN_PLUGIN_FUNC(openvpn_plugin_open_v2) (unsigned int *type_mask, const char *argv[], const char *envp[], struct openvpn_plugin_string_list **return_list); /* * FUNCTION: openvpn_plugin_func_v2 * * Called to perform the work of a given script type. * * REQUIRED: YES * * ARGUMENTS * * handle : the openvpn_plugin_handle_t value which was returned by * openvpn_plugin_open. * * type : one of the PLUGIN_x types * * argv : a NULL-terminated array of "command line" options which * would normally be passed to the script. argv[0] is the dynamic * library pathname. * * envp : a NULL-terminated array of OpenVPN-set environmental * variables in "name=value" format. Note that for security reasons, * these variables are not actually written to the "official" * environmental variable store of the process. * * per_client_context : the per-client context pointer which was returned by * openvpn_plugin_client_constructor_v1, if defined. * * return_list : used to return data back to OpenVPN. * * RETURN VALUE * * OPENVPN_PLUGIN_FUNC_SUCCESS on success, OPENVPN_PLUGIN_FUNC_ERROR on failure * * In addition, OPENVPN_PLUGIN_FUNC_DEFERRED may be returned by * OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY. This enables asynchronous * authentication where the plugin (or one of its agents) may indicate * authentication success/failure some number of seconds after the return * of the OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY handler by writing a single * char to the file named by auth_control_file in the environmental variable * list (envp). * * first char of auth_control_file: * '0' -- indicates auth failure * '1' -- indicates auth success * * OpenVPN will delete the auth_control_file after it goes out of scope. * * If an OPENVPN_PLUGIN_ENABLE_PF handler is defined and returns success * for a particular client instance, packet filtering will be enabled for that * instance. OpenVPN will then attempt to read the packet filter configuration * from the temporary file named by the environmental variable pf_file. This * file may be generated asynchronously and may be dynamically updated during the * client session, however the client will be blocked from sending or receiving * VPN tunnel packets until the packet filter file has been generated. OpenVPN * will periodically test the packet filter file over the life of the client * instance and reload when modified. OpenVPN will delete the packet filter file * when the client instance goes out of scope. * * Packet filter file grammar: * * [CLIENTS DROP|ACCEPT] * {+|-}common_name1 * {+|-}common_name2 * . . . * [SUBNETS DROP|ACCEPT] * {+|-}subnet1 * {+|-}subnet2 * . . . * [END] * * Subnet: IP-ADDRESS | IP-ADDRESS/NUM_NETWORK_BITS * * CLIENTS refers to the set of clients (by their common-name) which * this instance is allowed ('+') to connect to, or is excluded ('-') * from connecting to. Note that in the case of client-to-client * connections, such communication must be allowed by the packet filter * configuration files of both clients. * * SUBNETS refers to IP addresses or IP address subnets which this * instance may connect to ('+') or is excluded ('-') from connecting * to. * * DROP or ACCEPT defines default policy when there is no explicit match * for a common-name or subnet. The [END] tag must exist. A special * purpose tag called [KILL] will immediately kill the client instance. * A given client or subnet rule applies to both incoming and outgoing * packets. * * See plugin/defer/simple.c for an example on using asynchronous * authentication and client-specific packet filtering. */ OPENVPN_PLUGIN_DEF int OPENVPN_PLUGIN_FUNC(openvpn_plugin_func_v2) (openvpn_plugin_handle_t handle, const int type, const char *argv[], const char *envp[], void *per_client_context, struct openvpn_plugin_string_list **return_list); /* * FUNCTION: openvpn_plugin_open_v3 * * REQUIRED: YES * * Called on initial plug-in load. OpenVPN will preserve plug-in state * across SIGUSR1 restarts but not across SIGHUP restarts. A SIGHUP reset * will cause the plugin to be closed and reopened. * * ARGUMENTS * * version : fixed value, defines the API version of the OpenVPN plug-in API. The plug-in * should validate that this value is matching the OPENVPN_PLUGINv3_STRUCTVER * value. * * arguments : Structure with all arguments available to the plug-in. * * retptr : used to return data back to OpenVPN. * * RETURN VALUE * * OPENVPN_PLUGIN_FUNC_SUCCESS on success, OPENVPN_PLUGIN_FUNC_ERROR on failure */ OPENVPN_PLUGIN_DEF int OPENVPN_PLUGIN_FUNC(openvpn_plugin_open_v3) (const int version, struct openvpn_plugin_args_open_in const *arguments, struct openvpn_plugin_args_open_return *retptr); /* * FUNCTION: openvpn_plugin_func_v3 * * Called to perform the work of a given script type. * * REQUIRED: YES * * ARGUMENTS * * version : fixed value, defines the API version of the OpenVPN plug-in API. The plug-in * should validate that this value is matching the OPENVPN_PLUGINv3_STRUCTVER * value. * * arguments : Structure with all arguments available to the plug-in. * * retptr : used to return data back to OpenVPN. * * RETURN VALUE * * OPENVPN_PLUGIN_FUNC_SUCCESS on success, OPENVPN_PLUGIN_FUNC_ERROR on failure * * In addition, OPENVPN_PLUGIN_FUNC_DEFERRED may be returned by * OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY. This enables asynchronous * authentication where the plugin (or one of its agents) may indicate * authentication success/failure some number of seconds after the return * of the OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY handler by writing a single * char to the file named by auth_control_file in the environmental variable * list (envp). * * first char of auth_control_file: * '0' -- indicates auth failure * '1' -- indicates auth success * * OpenVPN will delete the auth_control_file after it goes out of scope. * * If an OPENVPN_PLUGIN_ENABLE_PF handler is defined and returns success * for a particular client instance, packet filtering will be enabled for that * instance. OpenVPN will then attempt to read the packet filter configuration * from the temporary file named by the environmental variable pf_file. This * file may be generated asynchronously and may be dynamically updated during the * client session, however the client will be blocked from sending or receiving * VPN tunnel packets until the packet filter file has been generated. OpenVPN * will periodically test the packet filter file over the life of the client * instance and reload when modified. OpenVPN will delete the packet filter file * when the client instance goes out of scope. * * Packet filter file grammar: * * [CLIENTS DROP|ACCEPT] * {+|-}common_name1 * {+|-}common_name2 * . . . * [SUBNETS DROP|ACCEPT] * {+|-}subnet1 * {+|-}subnet2 * . . . * [END] * * Subnet: IP-ADDRESS | IP-ADDRESS/NUM_NETWORK_BITS * * CLIENTS refers to the set of clients (by their common-name) which * this instance is allowed ('+') to connect to, or is excluded ('-') * from connecting to. Note that in the case of client-to-client * connections, such communication must be allowed by the packet filter * configuration files of both clients. * * SUBNETS refers to IP addresses or IP address subnets which this * instance may connect to ('+') or is excluded ('-') from connecting * to. * * DROP or ACCEPT defines default policy when there is no explicit match * for a common-name or subnet. The [END] tag must exist. A special * purpose tag called [KILL] will immediately kill the client instance. * A given client or subnet rule applies to both incoming and outgoing * packets. * * See sample/sample-plugins/defer/simple.c for an example on using * asynchronous authentication and client-specific packet filtering. */ OPENVPN_PLUGIN_DEF int OPENVPN_PLUGIN_FUNC(openvpn_plugin_func_v3) (const int version, struct openvpn_plugin_args_func_in const *arguments, struct openvpn_plugin_args_func_return *retptr); /* * FUNCTION: openvpn_plugin_close_v1 * * REQUIRED: YES * * ARGUMENTS * * handle : the openvpn_plugin_handle_t value which was returned by * openvpn_plugin_open. * * Called immediately prior to plug-in unload. */ OPENVPN_PLUGIN_DEF void OPENVPN_PLUGIN_FUNC(openvpn_plugin_close_v1) (openvpn_plugin_handle_t handle); /* * FUNCTION: openvpn_plugin_abort_v1 * * REQUIRED: NO * * ARGUMENTS * * handle : the openvpn_plugin_handle_t value which was returned by * openvpn_plugin_open. * * Called when OpenVPN is in the process of aborting due to a fatal error. * Will only be called on an open context returned by a prior successful * openvpn_plugin_open callback. */ OPENVPN_PLUGIN_DEF void OPENVPN_PLUGIN_FUNC(openvpn_plugin_abort_v1) (openvpn_plugin_handle_t handle); /* * FUNCTION: openvpn_plugin_client_constructor_v1 * * Called to allocate a per-client memory region, which * is then passed to the openvpn_plugin_func_v2 function. * This function is called every time the OpenVPN server * constructs a client instance object, which normally * occurs when a session-initiating packet is received * by a new client, even before the client has authenticated. * * This function should allocate the private memory needed * by the plugin to track individual OpenVPN clients, and * return a void * to this memory region. * * REQUIRED: NO * * ARGUMENTS * * handle : the openvpn_plugin_handle_t value which was returned by * openvpn_plugin_open. * * RETURN VALUE * * void * pointer to plugin's private per-client memory region, or NULL * if no memory region is required. */ OPENVPN_PLUGIN_DEF void *OPENVPN_PLUGIN_FUNC(openvpn_plugin_client_constructor_v1) (openvpn_plugin_handle_t handle); /* * FUNCTION: openvpn_plugin_client_destructor_v1 * * This function is called on client instance object destruction. * * REQUIRED: NO * * ARGUMENTS * * handle : the openvpn_plugin_handle_t value which was returned by * openvpn_plugin_open. * * per_client_context : the per-client context pointer which was returned by * openvpn_plugin_client_constructor_v1, if defined. */ OPENVPN_PLUGIN_DEF void OPENVPN_PLUGIN_FUNC(openvpn_plugin_client_destructor_v1) (openvpn_plugin_handle_t handle, void *per_client_context); /* * FUNCTION: openvpn_plugin_select_initialization_point_v1 * * Several different points exist in OpenVPN's initialization sequence where * the openvpn_plugin_open function can be called. While the default is * OPENVPN_PLUGIN_INIT_PRE_DAEMON, this function can be used to select a * different initialization point. For example, if your plugin needs to * return configuration parameters to OpenVPN, use * OPENVPN_PLUGIN_INIT_PRE_CONFIG_PARSE. * * REQUIRED: NO * * RETURN VALUE: * * An OPENVPN_PLUGIN_INIT_x value. */ #define OPENVPN_PLUGIN_INIT_PRE_CONFIG_PARSE 1 #define OPENVPN_PLUGIN_INIT_PRE_DAEMON 2 /* default */ #define OPENVPN_PLUGIN_INIT_POST_DAEMON 3 #define OPENVPN_PLUGIN_INIT_POST_UID_CHANGE 4 OPENVPN_PLUGIN_DEF int OPENVPN_PLUGIN_FUNC(openvpn_plugin_select_initialization_point_v1) (void); /* * FUNCTION: openvpn_plugin_min_version_required_v1 * * This function is called by OpenVPN to query the minimum * plugin interface version number required by the plugin. * * REQUIRED: NO * * RETURN VALUE * * The minimum OpenVPN plugin interface version number necessary to support * this plugin. */ OPENVPN_PLUGIN_DEF int OPENVPN_PLUGIN_FUNC(openvpn_plugin_min_version_required_v1) (void); /* * Deprecated functions which are still supported for backward compatibility. */ OPENVPN_PLUGIN_DEF openvpn_plugin_handle_t OPENVPN_PLUGIN_FUNC(openvpn_plugin_open_v1) (unsigned int *type_mask, const char *argv[], const char *envp[]); OPENVPN_PLUGIN_DEF int OPENVPN_PLUGIN_FUNC(openvpn_plugin_func_v1) (openvpn_plugin_handle_t handle, const int type, const char *argv[], const char *envp[]); #ifdef __cplusplus } #endif #endif /* OPENVPN_PLUGIN_H_ */ 07070100000029000041ED000000000000000000000004663BA04300000000000000000000000000000000000000000000001C00000000openvpn-auth-okta-2.8.2/pkg0707010000002A000041ED000000000000000000000002663BA04300000000000000000000000000000000000000000000002800000000openvpn-auth-okta-2.8.2/pkg/oktaApiAuth0707010000002B000081A4000000000000000000000001663BA04300002667000000000000000000000000000000000000002F00000000openvpn-auth-okta-2.8.2/pkg/oktaApiAuth/api.go// SPDX-FileCopyrightText: 2023-Present Algolia // // SPDX-License-Identifier: MPL-2.0 // // Copyright 2023-Present Algolia // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package oktaApiAuth import ( "bytes" "crypto/sha256" "crypto/tls" "crypto/x509" "encoding/base64" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "slices" "time" "github.com/go-playground/validator/v10" "github.com/phuslu/log" ) // Prepare an http client with a safe TLS config // validate the server public key against our list of pinned key fingerprint func (auth *OktaApiAuth) InitPool() error { log.Trace().Msg("oktaApiAuth.InitPool()") if rawURL, err := url.Parse(auth.ApiConfig.Url); err != nil { return err } else { var port string if port = rawURL.Port(); port == "" { port = "443" } // Connect to the server, fetch its public key and validate it against the // base64 digest in pinset slice tcpURL := fmt.Sprintf("%s:%s", rawURL.Hostname(), port) conn, err := tls.Dial("tcp", tcpURL, &tls.Config{InsecureSkipVerify: true}) if err != nil { log.Error().Msgf("Error in Dial: %s", err) return err } defer conn.Close() certs := conn.ConnectionState().PeerCertificates for _, cert := range certs { if !cert.IsCA { // Compute public key base64 digest derPubKey, err := x509.MarshalPKIXPublicKey(cert.PublicKey) if err != nil { return err } pubKeySha := sha256.Sum256(derPubKey) digest := base64.StdEncoding.EncodeToString([]byte(string(pubKeySha[:]))) if !slices.Contains(auth.ApiConfig.AssertPin, digest) { log.Error().Msgf("Refusing to authenticate because host %s failed %s\n%s\n%s", rawURL.Hostname(), "a TLS public key pinning check.", "Update your \"pinset.cfg\" file or ", "contact support@okta.com with this error message") return errors.New("Server pubkey does not match pinned keys") } } } } tlsCfg := &tls.Config{ InsecureSkipVerify: false, MinVersion: tls.VersionTLS12, CipherSuites: []uint16{ // TLS 1.2 safe cipher suites tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, // TLS 1.3 cipher suites tls.TLS_AES_128_GCM_SHA256, tls.TLS_AES_256_GCM_SHA384, tls.TLS_CHACHA20_POLY1305_SHA256, }, } t := &http.Transport{ MaxIdleConns: 5, MaxConnsPerHost: 5, MaxIdleConnsPerHost: 5, TLSClientConfig: tlsCfg, } auth.pool = &http.Client{ Timeout: 10 * time.Second, Transport: t, } return nil } // only used by validator_test.go // TODO: find a clean way to only export this for tests func (auth *OktaApiAuth) Pool() *http.Client { return auth.pool } // Do an http request to the Okta API using the path and payload provided func (auth *OktaApiAuth) oktaReq(method string, path string, data map[string]string) (code int, jsonBody []byte, err error) { u, _ := url.ParseRequestURI(auth.ApiConfig.Url) u.Path = fmt.Sprintf("/api/v1%s", path) ssws := fmt.Sprintf("SSWS %s", auth.ApiConfig.Token) headers := map[string]string{ "User-Agent": auth.userAgent, "Content-Type": "application/json", "Accept": "application/json", "Authorization": ssws, } if auth.UserConfig.ClientIp != "" { headers["X-Forwarded-For"] = auth.UserConfig.ClientIp } var r *http.Request var dataReader *bytes.Reader if method == http.MethodPost { jsonData, err := json.Marshal(data) if err != nil { log.Error().Msgf("Error marshaling request payload: %s", err) return 0, nil, err } dataReader = bytes.NewReader(jsonData) } else { dataReader = bytes.NewReader([]byte{}) } r, err = http.NewRequest(method, u.String(), dataReader) if err != nil { log.Error().Msgf("Error creating http request: %s", err) return 0, nil, err } for k, v := range headers { r.Header.Add(k, v) } resp, err := auth.pool.Do(r) if err != nil { return 0, nil, err } defer resp.Body.Close() jsonBody, err = io.ReadAll(resp.Body) if err != nil { log.Error().Msgf("Error reading Okta API response: %s", err) return 0, nil, err } return resp.StatusCode, jsonBody, nil } // Call the preauth Okta API endpoint func (auth *OktaApiAuth) preAuth() (int, []byte, error) { // https://developer.okta.com/docs/reference/api/authn/#primary-authentication-with-public-application log.Trace().Msg("oktaApiAuth.preAuth()") data := map[string]string{ "username": auth.UserConfig.Username, "password": auth.UserConfig.Password, } return auth.oktaReq(http.MethodPost, "/authn", data) } // Call the MFA auth Okta API endpoint func (auth *OktaApiAuth) doAuth(fid string, stateToken string) (int, []byte, error) { // https://developer.okta.com/docs/reference/api/authn/#verify-call-factor log.Trace().Msg("oktaApiAuth.doAuth()") path := fmt.Sprintf("/authn/factors/%s/verify", fid) data := map[string]string{ "fid": fid, "stateToken": stateToken, "passCode": auth.UserConfig.Passcode, } return auth.oktaReq(http.MethodPost, path, data) } // Cancel an authentication transaction func (auth *OktaApiAuth) cancelAuth(stateToken string) { // https://developer.okta.com/docs/reference/api/authn/#cancel-transaction log.Trace().Msg("oktaApiAuth.cancelAuth()") data := map[string]string{ "stateToken": stateToken, } _, _, _ = auth.oktaReq(http.MethodPost, "/authn/cancel", data) } func (auth *OktaApiAuth) doAuthFirstStep(factor AuthFactor, count int, nbFactors int, stateToken string, ftype string) (AuthResponse, error) { log.Trace().Msgf("oktaApiAuth.doAuthFirstStep() %s %s", factor.Type, factor.Provider) code, apiRes, err := auth.doAuth(factor.Id, stateToken) if err != nil { if count == nbFactors-1 { return AuthResponse{}, err } return AuthResponse{}, errors.New("continue") } validate := validator.New(validator.WithRequiredStructEnabled()) if code != 200 && code != 202 { var authResErr ErrorResponse err = json.Unmarshal(apiRes, &authResErr) var errorSummary string ferror := fmt.Sprintf("%s MFA failed", ftype) if err == nil { err = validate.Struct(authResErr) if err == nil { if len(authResErr.Causes) > 0 { errorSummary = authResErr.Causes[0].Summary } else { errorSummary = authResErr.Summary } } } else { errorSummary = fmt.Sprintf("HTTP status code %d", code) } if count == nbFactors-1 { log.Error().Msgf("%s %s MFA authentication failed: %s", factor.Provider, ftype, errorSummary) return AuthResponse{}, errors.New(ferror) } log.Warn().Msgf("%s %s MFA authentication failed: %s", factor.Provider, ftype, errorSummary) return AuthResponse{}, errors.New("continue") } var authRes AuthResponse err = json.Unmarshal(apiRes, &authRes) if err != nil { if count == nbFactors-1 { log.Error().Msgf("Error unmarshaling Okta API response: %s", err) return AuthResponse{}, err } log.Warn().Msgf("Error unmarshaling Okta API response: %s", err) return AuthResponse{}, errors.New("continue") } err = validate.Struct(authRes) if err != nil { if count == nbFactors-1 { log.Error().Msgf("Error unmarshaling Okta API response: %s", err) return AuthResponse{}, err } log.Warn().Msgf("Error unmarshaling Okta API response: %s", err) return AuthResponse{}, errors.New("continue") } return authRes, nil } // At first iteration and until the factorResult is different from WAITING // keep retrying the Push MFA (we are waiting here that the user either accept or reject auth) func (auth *OktaApiAuth) waitForPush(factor AuthFactor, count int, nbFactors int, stateToken string) (authRes AuthResponse, err error) { log.Trace().Msgf("oktaApiAuth.waitForPush() %s %s", factor.Type, factor.Provider) validate := validator.New(validator.WithRequiredStructEnabled()) checkCount := 0 for checkCount == 0 || authRes.Result == "WAITING" { checkCount++ if checkCount > auth.ApiConfig.MFAPushMaxRetries { if count == nbFactors-1 { log.Error().Msgf("%s push MFA timed out", factor.Provider) return AuthResponse{}, errors.New("Push MFA timeout") } log.Warn().Msgf("%s push MFA timed out", factor.Provider) return AuthResponse{}, errors.New("continue") } time.Sleep(time.Duration(auth.ApiConfig.MFAPushDelaySeconds) * time.Second) code, apiRes, err := auth.doAuth(factor.Id, stateToken) if err != nil { if count == nbFactors-1 { return AuthResponse{}, err } return AuthResponse{}, errors.New("continue") } if code != 200 && code != 202 { if count == nbFactors-1 { log.Error().Msgf("%s push MFA invalid HTTP status code %d", factor.Provider, code) return AuthResponse{}, errors.New("Push MFA failed") } log.Warn().Msgf("%s push MFA invalid HTTP status code %d", factor.Provider, code) return AuthResponse{}, errors.New("continue") } authRes = AuthResponse{} err = json.Unmarshal(apiRes, &authRes) if err != nil { if count == nbFactors-1 { log.Error().Msgf("Error unmarshaling Okta API response: %s", err) return AuthResponse{}, err } log.Warn().Msgf("Error unmarshaling Okta API response: %s", err) return AuthResponse{}, errors.New("continue") } err = validate.Struct(authRes) if err != nil { if count == nbFactors-1 { log.Error().Msgf("Error unmarshaling Okta API response: %s", err) return AuthResponse{}, err } log.Warn().Msgf("Error unmarshaling Okta API response: %s", err) return AuthResponse{}, errors.New("continue") } } return authRes, nil } 0707010000002C000081A4000000000000000000000001663BA043000011CA000000000000000000000000000000000000003400000000openvpn-auth-okta-2.8.2/pkg/oktaApiAuth/api_test.go// SPDX-FileCopyrightText: 2023-Present Algolia // // SPDX-License-Identifier: MPL-2.0 // // Copyright 2023-Present Algolia // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package oktaApiAuth import ( "crypto/tls" "fmt" "net/http" "testing" "time" "github.com/stretchr/testify/assert" "gopkg.in/h2non/gock.v1" ) type poolTest struct { testName string host string port string pinset []string err error } type setupTest struct { testName string requests []authRequest err error } func startTLS(t *testing.T) { t.Helper() mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("This is an example server.\n")) }) cfg := &tls.Config{MinVersion: tls.VersionTLS12} s := http.Server{ Addr: fmt.Sprintf("%s:%s", tlsHost, tlsPort), Handler: mux, TLSConfig: cfg, ReadTimeout: 1 * time.Second, WriteTimeout: 1 * time.Second, } err := s.ListenAndServeTLS("../../testing/fixtures/utils/server.crt", "../../testing/fixtures/utils/server.key") assert.NoError(t, err) t.Cleanup(func() { _ = s.Close() }) } func TestInitPool(t *testing.T) { invalidHost := "invalid{host" invalidHostErr := fmt.Sprintf("parse \"https://%s:%s\": invalid character \"{\" in host name", invalidHost, tlsPort) tests := []poolTest{ { "Test valid pinset", tlsHost, tlsPort, []string{validPinset}, nil, }, { "Test invalid pinset", tlsHost, tlsPort, []string{invalidPinset}, fmt.Errorf("Server pubkey does not match pinned keys"), }, { "Test unreachable host", tlsHost, "1444", []string{}, fmt.Errorf(fmt.Sprintf("dial tcp %s:1444: connect: connection refused", tlsHost)), }, { "Test invalid url", invalidHost, tlsPort, []string{}, fmt.Errorf(invalidHostErr), }, } go func() { startTLS(t) }() time.Sleep(1 * time.Second) for _, test := range tests { t.Run(test.testName, func(t *testing.T) { a := New() a.ApiConfig.Url = fmt.Sprintf("https://%s:%s", test.host, test.port) a.ApiConfig.AssertPin = test.pinset err := a.InitPool() if test.err == nil { if err != nil { t.Logf(err.Error()) } assert.Nil(t, err) } else { assert.Equal(t, test.err.Error(), err.Error()) } }) } } func TestOktaReq(t *testing.T) { defer gock.Off() // Uncomment the following line to see HTTP requests intercepted by gock //gock.Observe(gock.DumpRequest) tests := []setupTest{ { "invalid json response - failure", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusInternalServerError, "invalid.json", }, }, nil, }, { "invalid payload - failure", []authRequest{ { "/api/v1/authn", nil, http.StatusInternalServerError, "invalid.json", }, }, nil, }, } for _, test := range tests { t.Run(test.testName, func(t *testing.T) { gock.Clean() gock.Flush() apiCfg := &OktaAPIConfig{ Url: oktaEndpoint, Token: token, UsernameSuffix: "algolia.com", AssertPin: pin, MFARequired: false, AllowUntrustedUsers: true, MFAPushMaxRetries: 20, MFAPushDelaySeconds: 3, } userCfg := &OktaUserConfig{ Username: username, Password: password, Passcode: "", ClientIp: ip, } for _, req := range test.requests { reqponseFile := fmt.Sprintf("../../testing/fixtures/oktaApi/%s", req.jsonResponseFile) l := gock.New(oktaEndpoint) l = l.Post(req.path). MatchHeader("Authorization", fmt.Sprintf("SSWS %s", token)). MatchHeader("X-Forwarded-For", ip). MatchType("json"). JSON(req.payload) l.Reply(req.httpStatus). File(reqponseFile) } a := New() assert.NotNil(t, a) a.ApiConfig = apiCfg a.UserConfig = userCfg err := a.InitPool() assert.Nil(t, err) gock.InterceptClient(a.pool) // Lets ensure we wont reach the real okta API gock.DisableNetworking() _, _, err = a.oktaReq(http.MethodPost, test.requests[0].path, test.requests[0].payload) if test.err == nil { assert.Nil(t, err) } else { assert.Equal(t, test.err.Error(), err.Error()) } }) } } 0707010000002D000081A4000000000000000000000001663BA04300000691000000000000000000000000000000000000003500000000openvpn-auth-okta-2.8.2/pkg/oktaApiAuth/api_types.go// SPDX-FileCopyrightText: 2023-Present Algolia // // SPDX-License-Identifier: MPL-2.0 // // Copyright 2024-Present Algolia // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package oktaApiAuth type ErrorResponse struct { Code string `json:"errorCode" validate:"required"` Summary string `json:"errorSummary" validate:"required"` Link string `json:"errorLink" validate:"required"` Id string `json:"errorId" validate:"required"` Causes []ErrorCauses `json:"errorCauses" validate:"required"` } type ErrorCauses struct { Summary string `json:"errorSummary"` } type PreAuthResponse struct { Status string `json:"status" validate:"required"` Token string `json:"stateToken"` Embedded PreAuthEmbedded `json:"_embedded"` } type PreAuthEmbedded struct { Factors []AuthFactor `json:"factors"` } type AuthFactor struct { Id string `json:"id" validate:"required"` Type string `json:"factorType" validate:"required"` Provider string `json:"provider" validate:"required"` } type AuthResponse struct { Status string `json:"status" validate:"required"` Token string `json:"stateToken"` Result string `json:"factorResult"` } type OktaGroups struct { Groups []OktaGroup `json:"groups" validate:"omitempty,dive"` } type OktaGroup struct { Id string `json:"id" validate:"required"` Profile OktaGroupProfile `json:"profile" validate:"required"` } type OktaGroupProfile struct { Name string `json:"name" validate:"required"` } 0707010000002E000081A4000000000000000000000001663BA043000014EB000000000000000000000000000000000000003700000000openvpn-auth-okta-2.8.2/pkg/oktaApiAuth/oktaApiAuth.go// SPDX-FileCopyrightText: 2023-Present Algolia // // SPDX-License-Identifier: MPL-2.0 // // Copyright 2023-Present Algolia // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package oktaApiAuth import ( "errors" "fmt" "github.com/phuslu/log" ) // Returns an initialized oktaApiAuth func New() *OktaApiAuth { /* utsname := unix.Utsname{} _ = unix.Uname(&utsname) userAgent := fmt.Sprintf("OktaOpenVPN/2.1.0 (%s %s) Go-http-client/%s", utsname.Sysname, utsname.Release, runtime.Version()[2:]) fmt.Printf("agent: %s\n", userAgent) using dynamic user agent does not work .... so for now use a const var */ return &OktaApiAuth{ ApiConfig: &OktaAPIConfig{ AllowUntrustedUsers: false, MFARequired: false, MFAPushMaxRetries: 20, MFAPushDelaySeconds: 3, AllowedGroups: "", TOTPFallbackToPush: false, }, UserConfig: &OktaUserConfig{}, userAgent: userAgent, } } // Iterates on the factor list provided and tries to authenticate the user // exit with no error at the first successful factor auth func (auth *OktaApiAuth) verifyFactors(stateToken string, factors []AuthFactor, factorType string) (err error) { log.Trace().Msgf("oktaApiAuth.verifyFactors() %s", factorType) nbFactors := len(factors) for count, factor := range factors { log.Debug().Msgf("verifying %s factor nb %d", factorType, count) authRes, err := auth.doAuthFirstStep(factor, count, nbFactors, stateToken, factorType) if err != nil { if err.Error() != "continue" { return err } continue } log.Debug().Msgf("%s %s MFA, Result: %s", factor.Provider, factorType, authRes.Result) if factorType == "Push" { if authRes.Result != "WAITING" { if count == nbFactors-1 { return errors.New("Push MFA failed") } continue } authRes, err = auth.waitForPush(factor, count, nbFactors, stateToken) if err != nil { if err.Error() != "continue" { return err } continue } log.Debug().Msgf("%s Push MFA, waitForPush Result: %s", factor.Provider, authRes.Result) } if authRes.Status == "SUCCESS" { log.Info().Msgf("authenticated with %s %s MFA", factor.Provider, factorType) return nil } if count == nbFactors-1 { log.Error().Msgf("%s %s MFA authentication failed: %s", factor.Provider, factorType, authRes.Result) return fmt.Errorf("%s MFA failed", factorType) } log.Warn().Msgf("%s %s MFA authentication failed: %s", factor.Provider, factorType, authRes.Result) } // Reached only when the list of factors provided is empty log.Debug().Msgf("No %s MFA available", factorType) return fmt.Errorf("No %s MFA available", factorType) } // Gather the list of factors available from the pre authentication api response, // if the user provided a TOTP in its passwordd string, try TOTP MFA // otherwise try Push MFA func (auth *OktaApiAuth) validateUserMFA(preAuthRes PreAuthResponse) (err error) { log.Trace().Msg("oktaApiAuth.validateUserMFA()") factorsTOTP, factorsPush := auth.getUserFactors(preAuthRes) if auth.UserConfig.Passcode != "" { if err = auth.verifyFactors(preAuthRes.Token, factorsTOTP, "TOTP"); err != nil { if auth.ApiConfig.TOTPFallbackToPush { // If all TOTP factors failed and fallback to push has been enabled in config // try Push MFA authentication goto PUSH } if err.Error() != "No TOTP MFA available" { auth.cancelAuth(preAuthRes.Token) return err } goto ERR } return nil } PUSH: if err = auth.verifyFactors(preAuthRes.Token, factorsPush, "Push"); err != nil { if err.Error() != "No Push MFA available" { auth.cancelAuth(preAuthRes.Token) return err } goto ERR } return nil ERR: log.Error().Msgf("No MFA factor available") auth.cancelAuth(preAuthRes.Token) return errors.New("No MFA factor available") } // Do a full authentication transaction: preAuth, doAuth (when needed), cancelAuth (when needed) // returns nil if has been validated by Okta API, an error otherwise func (auth *OktaApiAuth) Auth() error { log.Trace().Msg("oktaApiAuth.Auth()") log.Info().Msgf("Authenticating") preAuthRes, err := auth.preChecks() if err != nil { return err } switch preAuthRes.Status { case "SUCCESS": if auth.ApiConfig.MFARequired { log.Warn().Msgf("allowed without MFA but MFA is required - rejected") return errors.New("MFA required") } return nil case "LOCKED_OUT": log.Warn().Msgf("is locked out") return errors.New("User locked out") case "PASSWORD_EXPIRED": log.Warn().Msgf("password is expired") if preAuthRes.Token != "" { auth.cancelAuth(preAuthRes.Token) } return errors.New("User password expired") case "MFA_ENROLL", "MFA_ENROLL_ACTIVATE": log.Warn().Msgf("needs to enroll first") if preAuthRes.Token != "" { auth.cancelAuth(preAuthRes.Token) } return errors.New("Needs to enroll") case "MFA_REQUIRED", "MFA_CHALLENGE": log.Debug().Msgf("checking second factor") return auth.validateUserMFA(preAuthRes) default: log.Error().Msgf("unknown preauth status: %s", preAuthRes.Status) if preAuthRes.Token != "" { auth.cancelAuth(preAuthRes.Token) } return errors.New("Unknown preauth status") } } 0707010000002F000081A4000000000000000000000001663BA04300009E6A000000000000000000000000000000000000003C00000000openvpn-auth-okta-2.8.2/pkg/oktaApiAuth/oktaApiAuth_test.go// SPDX-FileCopyrightText: 2023-Present Algolia // // SPDX-License-Identifier: MPL-2.0 // // Copyright 2023-Present Algolia // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package oktaApiAuth import ( "fmt" "net/http" "testing" "github.com/stretchr/testify/assert" "gopkg.in/h2non/gock.v1" ) const ( // Please update the fixtures if you modify one of this var !! oktaEndpoint string = "https://example.oktapreview.com" token string = "12345" username string = "dade.murphy@example.com" password string = "test_password" passcode string = "987654" ip string = "1.2.3.4" stateToken string = "007ucIX7PATyn94hsHfOLVaXAmOBkKHWnOOLG43bsb" pushFID string = "opf3hkfocI4JTLAju0g4" totpFID string = "ostfm3hPNYSOIOIVTQWY" // validPinset has been computed using: /* cat testing/fixtures/server.crt |\ openssl x509 -noout -pubkey |\ openssl rsa -pubin -outform der 2>/dev/null |\ openssl dgst -sha256 -binary | base64 */ tlsHost string = "127.0.0.1" tlsPort string = "1443" validPinset string = "j69yToSVkR6G7RKEc0qvsA6MysH+luI3wBIihDA20nI=" invalidPinset string = "ABCDEF" ) type authTest struct { testName string mfaRequired bool passcode string requests []authRequest unmatchedReq bool allowedGroups string pushRetries int fallback bool err error } /* all the JSON response files used here have been extracted from https://developer.okta.com/docs/reference/api/authn/#primary-authentication-with-public-application https://developer.okta.com/docs/reference/api/authn/#multifactor-authentication-operations with fatcor id and stateToken modifications */ func commonAuthTest(authTests []authTest, t *testing.T) { defer gock.Off() // Uncomment the following line to see HTTP requests intercepted by gock //gock.Observe(gock.DumpRequest) for _, test := range authTests { t.Run(test.testName, func(t *testing.T) { gock.Clean() gock.CleanUnmatchedRequest() gock.Flush() apiCfg := &OktaAPIConfig{ Url: oktaEndpoint, Token: token, UsernameSuffix: "algolia.com", AssertPin: pin, MFARequired: test.mfaRequired, AllowUntrustedUsers: true, MFAPushMaxRetries: test.pushRetries, MFAPushDelaySeconds: 0, AllowedGroups: test.allowedGroups, TOTPFallbackToPush: test.fallback, } userCfg := &OktaUserConfig{ Username: username, Password: password, Passcode: test.passcode, ClientIp: ip, } for _, req := range test.requests { responseFile := fmt.Sprintf("../../testing/fixtures/oktaApi/%s", req.jsonResponseFile) l := gock.New(oktaEndpoint) if test.allowedGroups != "" { l = l.Get(req.path). MatchHeader("Authorization", fmt.Sprintf("SSWS %s", token)). MatchHeader("X-Forwarded-For", ip). MatchType("json") l.Reply(req.httpStatus). File(responseFile) } else { l = l.Post(req.path). MatchHeader("Authorization", fmt.Sprintf("SSWS %s", token)). MatchHeader("X-Forwarded-For", ip). MatchType("json"). JSON(req.payload) l.Reply(req.httpStatus). File(responseFile) } } a := &OktaApiAuth{ ApiConfig: apiCfg, UserConfig: userCfg, userAgent: userAgent, } err := a.InitPool() assert.Nil(t, err) gock.InterceptClient(a.pool) gock.DisableNetworking() err2 := a.Auth() if test.err == nil { assert.Nil(t, err2) } else { assert.Equal(t, test.err.Error(), err2.Error()) } if !test.unmatchedReq { assert.False(t, gock.HasUnmatchedRequest()) } if test.testName == "PreAuth connection issue - failure" { assert.True(t, gock.IsPending()) assert.False(t, gock.IsDone()) } else { assert.False(t, gock.IsPending()) assert.True(t, gock.IsDone()) } }) } } func TestAuthGroups(t *testing.T) { authTests := []authTest{ { "Not member of allowed groups - failure", false, "", []authRequest{ { fmt.Sprintf("/api/v1/users/%s/groups", username), map[string]string{"username": username, "password": password}, http.StatusOK, "groups.json", }, }, false, "test1, test2", 1, false, fmt.Errorf("Not mmember of an AllowedGroup"), }, } commonAuthTest(authTests, t) } func TestAuthPreAuth(t *testing.T) { authTests := []authTest{ { "PreAuth connection issue - failure", true, passcode, []authRequest{ { "/api/v1/authn/cancel", map[string]string{"stateToken": stateToken}, http.StatusOK, "empty.json", }, }, true, "", 1, false, fmt.Errorf("Post \"https://example.oktapreview.com/api/v1/authn\": gock: cannot match any request"), }, { "PreAuth rate limited - failure", false, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusTooManyRequests, "preauth_rate_limit.json", }, }, false, "", 1, false, fmt.Errorf("pre-authentication rate limited"), }, { "PreAuth with invalid creadential, (hidden) locked out user - failure", false, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusUnauthorized, "preauth_invalid_credentials.json", }, }, false, "", 1, false, fmt.Errorf("pre-authentication failed"), }, { "PreAuth with invalid response (missing status) - failure", false, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_missing_status.json", }, }, false, "", 1, false, fmt.Errorf("Key: 'PreAuthResponse.Status' Error:Field validation for 'Status' failed on the 'required' tag"), }, { "PreAuth with invalid response (json) - failure", false, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "invalid.json", }, }, false, "", 1, false, fmt.Errorf("invalid character '-' looking for beginning of object key string"), }, { "PreAuth with locked out user - failure", false, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusUnauthorized, "preauth_lockedout.json", }, }, false, "", 1, false, fmt.Errorf("User locked out"), }, { "PreAuth with password expired - failure", false, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_password_expired.json", }, { "/api/v1/authn/cancel", map[string]string{"stateToken": stateToken}, http.StatusOK, "empty.json", }, }, false, "", 1, false, fmt.Errorf("User password expired"), }, { "PreAuth successful with no MFA required - success", false, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_success_without_mfa.json", }, }, false, "", 1, false, nil, }, { "PreAuth successful but MFA required - failure", true, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_success_without_mfa.json", }, }, false, "", 1, false, fmt.Errorf("MFA required"), }, { "PreAuth with MFA enrollment needed - failure", true, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_mfa_enroll.json", }, { "/api/v1/authn/cancel", map[string]string{"stateToken": stateToken}, http.StatusOK, "empty.json", }, }, false, "", 1, false, fmt.Errorf("Needs to enroll"), }, { "PreAuth with unknown status - failure", true, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_unknown_status.json", }, { "/api/v1/authn/cancel", map[string]string{"stateToken": stateToken}, http.StatusOK, "empty.json", }, }, false, "", 1, false, fmt.Errorf("Unknown preauth status"), }, } commonAuthTest(authTests, t) } func TestAuthMFA(t *testing.T) { authTests := []authTest{ { "Auth with MFA required, HTTP err - failure", true, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_push_mfa_required.json", }, }, true, "", 1, false, fmt.Errorf("Post \"https://example.oktapreview.com/api/v1/authn/factors/opf3hkfocI4JTLAju0g4/verify\": gock: cannot match any request"), }, { "Auth with unknown factortype - failure", true, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_unknown_mfa_required.json", }, { "/api/v1/authn/cancel", map[string]string{"stateToken": stateToken}, http.StatusOK, "empty.json", }, }, false, "", 1, false, fmt.Errorf("No MFA factor available"), }, { "Auth with unknown factortype, passcode provided - failure", true, passcode, []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_unknown_mfa_required.json", }, { "/api/v1/authn/cancel", map[string]string{"stateToken": stateToken}, http.StatusOK, "empty.json", }, }, false, "", 1, false, fmt.Errorf("No MFA factor available"), }, } commonAuthTest(authTests, t) } func TestAuthPushMFA(t *testing.T) { authTests := []authTest{ { "Auth with push MFA required - success", true, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_push_mfa_required.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_success.json", }, }, false, "", 1, false, nil, }, { "Auth with push MFA rejected - failure", true, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_push_mfa_required.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_rejected_push.json", }, { "/api/v1/authn/cancel", map[string]string{"stateToken": stateToken}, http.StatusOK, "empty.json", }, }, false, "", 1, false, fmt.Errorf("Push MFA failed"), }, { "Auth with push MFA rejected during wait - failure", true, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_push_mfa_required.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_rejected_push.json", }, { "/api/v1/authn/cancel", map[string]string{"stateToken": stateToken}, http.StatusOK, "empty.json", }, }, false, "", 1, false, fmt.Errorf("Push MFA failed"), }, { "Auth with 2 push MFA required - failure", true, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_2_push_mfa_providers.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", "rejected"), map[string]string{"fid": "rejected", "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", "rejected"), map[string]string{"fid": "rejected", "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_rejected_push.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_rejected_push.json", }, { "/api/v1/authn/cancel", map[string]string{"stateToken": stateToken}, http.StatusOK, "empty.json", }, }, false, "", 1, false, fmt.Errorf("Push MFA failed"), }, { "Auth with 2 push MFA rejected - failure", true, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_2_push_mfa_providers.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", "rejected"), map[string]string{"fid": "rejected", "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_rejected_push.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_rejected_push.json", }, { "/api/v1/authn/cancel", map[string]string{"stateToken": stateToken}, http.StatusOK, "empty.json", }, }, false, "", 1, false, fmt.Errorf("Push MFA failed"), }, { "Auth with push timeout err - failure", true, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_push_mfa_required.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, }, true, "", 1, false, fmt.Errorf("Post \"https://example.oktapreview.com/api/v1/authn/factors/opf3hkfocI4JTLAju0g4/verify\": gock: cannot match any request"), }, { "Auth with push, missing status - failure", true, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_push_mfa_required.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_missing_status.json", }, { "/api/v1/authn/cancel", map[string]string{"stateToken": stateToken}, http.StatusOK, "empty.json", }, }, true, "", 1, false, fmt.Errorf("Key: 'AuthResponse.Status' Error:Field validation for 'Status' failed on the 'required' tag"), }, { "Auth with push, missing status during wait - failure", true, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_push_mfa_required.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_missing_status.json", }, { "/api/v1/authn/cancel", map[string]string{"stateToken": stateToken}, http.StatusOK, "empty.json", }, }, false, "", 2, false, fmt.Errorf("Key: 'AuthResponse.Status' Error:Field validation for 'Status' failed on the 'required' tag"), }, { "Auth with push, http error during wait - failure", true, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_push_mfa_required.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusInternalServerError, "empty.json", }, { "/api/v1/authn/cancel", map[string]string{"stateToken": stateToken}, http.StatusOK, "empty.json", }, }, true, "", 1, false, fmt.Errorf("Push MFA failed"), }, { "Auth with push timeout during wait - failure", true, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_push_mfa_required.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { "/api/v1/authn/cancel", map[string]string{"stateToken": stateToken}, http.StatusOK, "empty.json", }, }, false, "", 1, false, fmt.Errorf("Push MFA timeout"), }, { "Auth with 2 push providers, first timeout - success", true, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_2_push_mfa_providers.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", "rejected"), map[string]string{"fid": "rejected", "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", "rejected"), map[string]string{"fid": "rejected", "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_success.json", }, }, false, "", 1, false, nil, }, { "Auth with 2 push, first missing status - success", true, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_2_push_mfa_providers.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", "rejected"), map[string]string{"fid": "rejected", "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_missing_status.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_success.json", }, }, false, "", 2, false, nil, }, { "Auth with 2 push, first no answer during wait - success", true, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_2_push_mfa_providers.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", "rejected"), map[string]string{"fid": "rejected", "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_success.json", }, }, true, "", 2, false, nil, }, { "Auth with 2 push, first missing status during wait - success", true, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_2_push_mfa_providers.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", "rejected"), map[string]string{"fid": "rejected", "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", "rejected"), map[string]string{"fid": "rejected", "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_missing_status.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_success.json", }, }, false, "", 2, false, nil, }, { "Auth with 2 push, both missing status during wait - failure", true, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_2_push_mfa_providers.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", "rejected"), map[string]string{"fid": "rejected", "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", "rejected"), map[string]string{"fid": "rejected", "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_missing_status.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_missing_status.json", }, { "/api/v1/authn/cancel", map[string]string{"stateToken": stateToken}, http.StatusOK, "empty.json", }, }, false, "", 2, false, fmt.Errorf("Key: 'AuthResponse.Status' Error:Field validation for 'Status' failed on the 'required' tag"), }, { "Auth with 2 push providers, first timeout - success", true, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_2_push_mfa_providers.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", "rejected"), map[string]string{"fid": "rejected", "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", "rejected"), map[string]string{"fid": "rejected", "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_success.json", }, }, false, "", 1, false, nil, }, { "Auth with 2 push providers, first http error during wait - success", true, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_2_push_mfa_providers.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", "rejected"), map[string]string{"fid": "rejected", "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", "rejected"), map[string]string{"fid": "rejected", "stateToken": stateToken, "passCode": ""}, http.StatusInternalServerError, "empty.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_success.json", }, }, false, "", 1, false, nil, }, { "Auth with 2 push providers, first invalid response - success", true, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_2_push_mfa_providers.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", "rejected"), map[string]string{"fid": "rejected", "stateToken": stateToken, "passCode": ""}, http.StatusOK, "invalid.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_success.json", }, }, false, "", 1, false, nil, }, { "Auth with 2 push providers, first server error - success", true, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_2_push_mfa_providers.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", "rejected"), map[string]string{"fid": "rejected", "stateToken": stateToken, "passCode": ""}, http.StatusInternalServerError, "invalid.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_success.json", }, }, false, "", 1, false, nil, }, { "Auth with 2 push providers, first no response - success", true, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_2_push_mfa_providers.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_success.json", }, }, true, "", 1, false, nil, }, { "Auth with 1 push providers, invalid response after waiting - failure", true, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_push_mfa_required.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "invalid.json", }, { "/api/v1/authn/cancel", map[string]string{"stateToken": stateToken}, http.StatusOK, "empty.json", }, }, false, "", 2, false, fmt.Errorf("invalid character '-' looking for beginning of object key string"), }, { "Auth with 2 push providers, first invalid response after waiting - success", true, "", []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_2_push_mfa_providers.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", "rejected"), map[string]string{"fid": "rejected", "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", "rejected"), map[string]string{"fid": "rejected", "stateToken": stateToken, "passCode": ""}, http.StatusOK, "invalid.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": ""}, http.StatusOK, "auth_success.json", }, }, false, "", 1, false, nil, }, } commonAuthTest(authTests, t) } func TestAuthTOTPMFA(t *testing.T) { authTests := []authTest{ { "Auth with TOTP MFA required - success", true, passcode, []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_totp_mfa_required.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", totpFID), map[string]string{"fid": totpFID, "stateToken": stateToken, "passCode": passcode}, http.StatusOK, "auth_success.json", }, }, false, "", 1, false, nil, }, { "Auth with TOTP MFA, invalid answer - failure", true, passcode, []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_totp_mfa_required.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", totpFID), map[string]string{"fid": totpFID, "stateToken": stateToken, "passCode": passcode}, http.StatusOK, "invalid.json", }, { "/api/v1/authn/cancel", map[string]string{"stateToken": stateToken}, http.StatusOK, "empty.json", }, }, false, "", 1, false, fmt.Errorf("invalid character '-' looking for beginning of object key string"), }, { "Auth with TOTP MFA required, multi MFA allowed to sort - success", true, passcode, []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_mfa_required_multi.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", totpFID), map[string]string{"fid": totpFID, "stateToken": stateToken, "passCode": passcode}, http.StatusOK, "auth_success.json", }, }, false, "", 1, false, nil, }, { "Auth with invalid TOTP MFA - failure", true, passcode, []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_totp_mfa_required.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", totpFID), map[string]string{"fid": totpFID, "stateToken": stateToken, "passCode": passcode}, http.StatusForbidden, "auth_invalid_totp.json", }, { "/api/v1/authn/cancel", map[string]string{"stateToken": stateToken}, http.StatusOK, "empty.json", }, }, false, "", 1, false, fmt.Errorf("TOTP MFA failed"), }, { "Auth with invalid TOTP MFA and no summary - failure", true, passcode, []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_totp_mfa_required.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", totpFID), map[string]string{"fid": totpFID, "stateToken": stateToken, "passCode": passcode}, http.StatusForbidden, "auth_invalid_totp_no_sum.json", }, { "/api/v1/authn/cancel", map[string]string{"stateToken": stateToken}, http.StatusOK, "empty.json", }, }, false, "", 1, false, fmt.Errorf("TOTP MFA failed"), }, { "Auth with 2 invalid TOTP MFA - failure", true, passcode, []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_2_totp_providers.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", "rejected"), map[string]string{"fid": "rejected", "stateToken": stateToken, "passCode": passcode}, http.StatusForbidden, "auth_invalid_totp.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", totpFID), map[string]string{"fid": totpFID, "stateToken": stateToken, "passCode": passcode}, http.StatusForbidden, "auth_invalid_totp.json", }, { "/api/v1/authn/cancel", map[string]string{"stateToken": stateToken}, http.StatusOK, "empty.json", }, }, false, "", 1, false, fmt.Errorf("TOTP MFA failed"), }, { "Auth with TOTP MFA missing status - failure", true, passcode, []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_totp_mfa_required.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", totpFID), map[string]string{"fid": totpFID, "stateToken": stateToken, "passCode": passcode}, http.StatusOK, "auth_missing_status.json", }, { "/api/v1/authn/cancel", map[string]string{"stateToken": stateToken}, http.StatusOK, "empty.json", }, }, false, "", 1, false, fmt.Errorf("Key: 'AuthResponse.Status' Error:Field validation for 'Status' failed on the 'required' tag"), }, { "Auth with 2 TOTP MFA, first invalid answer - success", true, passcode, []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_2_totp_providers.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", "rejected"), map[string]string{"fid": "rejected", "stateToken": stateToken, "passCode": passcode}, http.StatusOK, "invalid.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", totpFID), map[string]string{"fid": totpFID, "stateToken": stateToken, "passCode": passcode}, http.StatusOK, "auth_success.json", }, }, false, "", 1, false, nil, }, { "Auth with 2 TOTP MFA, first invalid answer - success", true, passcode, []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_2_totp_providers.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", "rejected"), map[string]string{"fid": "rejected", "stateToken": stateToken, "passCode": passcode}, http.StatusOK, "invalid.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", totpFID), map[string]string{"fid": totpFID, "stateToken": stateToken, "passCode": passcode}, http.StatusOK, "auth_success.json", }, }, false, "", 1, false, nil, }, { "Auth with 2 TOTP MFA, first no passcode - success", true, passcode, []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_2_totp_providers.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", totpFID), map[string]string{"fid": totpFID, "stateToken": stateToken, "passCode": passcode}, http.StatusOK, "auth_success.json", }, }, true, "", 1, false, nil, }, } commonAuthTest(authTests, t) } func TestAuthMFAFallback(t *testing.T) { authTests := []authTest{ { "Auth with invalid TOTP MFA, fallback to Push - success", true, passcode, []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_totp_push_providers.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", totpFID), map[string]string{"fid": totpFID, "stateToken": stateToken, "passCode": passcode}, http.StatusForbidden, "auth_invalid_totp.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": passcode}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": passcode}, http.StatusOK, "auth_success.json", }, }, false, "", 1, true, nil, }, { "Auth with invalid TOTP MFA, fallback to Push rejected - failure", true, passcode, []authRequest{ { "/api/v1/authn", map[string]string{"username": username, "password": password}, http.StatusOK, "preauth_totp_push_providers.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", totpFID), map[string]string{"fid": totpFID, "stateToken": stateToken, "passCode": passcode}, http.StatusForbidden, "auth_invalid_totp.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": passcode}, http.StatusOK, "auth_waiting.json", }, { fmt.Sprintf("/api/v1/authn/factors/%s/verify", pushFID), map[string]string{"fid": pushFID, "stateToken": stateToken, "passCode": passcode}, http.StatusOK, "auth_rejected_push.json", }, { "/api/v1/authn/cancel", map[string]string{"stateToken": stateToken}, http.StatusOK, "empty.json", }, }, false, "", 1, true, fmt.Errorf("Push MFA failed"), }, } commonAuthTest(authTests, t) } 07070100000030000081A4000000000000000000000001663BA043000007C7000000000000000000000000000000000000003100000000openvpn-auth-okta-2.8.2/pkg/oktaApiAuth/types.go// SPDX-FileCopyrightText: 2023-Present Algolia // // SPDX-License-Identifier: MPL-2.0 // // Copyright 2023-Present Algolia // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package oktaApiAuth import "net/http" const userAgent string = "Mozilla/5.0 (Linux; x86_64) OktaOpenVPN/2.1.0" // Contains the configuration for the Okta API connection // Those configuration options are read from api.ini type OktaAPIConfig struct { // Okta API server url, ie https://example.oktapreview.com Url string // Your (company's) Okta API token Token string // The suffix to be added to your users names: // ie if UsernameSuffix = "example.com" and your user logs in with "dade.murphy" // the validator will try to authenticate for "dade.murphy@example.com" UsernameSuffix string // A list of valid SSL public key fingerprint to validate the Okta API server certificate against AssertPin []string // Is MFA Required for all users. If yes and Okta authenticates the user without MFA (not configured) // the validator will reject it. MFARequired bool // default: false // Do not require usernames to come from client-side SSL certificates AllowUntrustedUsers bool // default: false // Number of retries when waiting for MFA result MFAPushMaxRetries int // default: 20 // Number of seconds to wait between MFA result retrieval tries MFAPushDelaySeconds int // default: 3 // List (comma separated) of groups allowed to connect AllowedGroups string // If a passcode is provided and TOTP MFA fails, try Push MFA TOTPFallbackToPush bool // default: false } // User credentials and informations type OktaUserConfig struct { Username string Password string Passcode string ClientIp string } type OktaApiAuth struct { ApiConfig *OktaAPIConfig UserConfig *OktaUserConfig pool *http.Client userAgent string } 07070100000031000081A4000000000000000000000001663BA04300000389000000000000000000000000000000000000003600000000openvpn-auth-okta-2.8.2/pkg/oktaApiAuth/types_test.go// SPDX-FileCopyrightText: 2023-Present Algolia // // SPDX-License-Identifier: MPL-2.0 // // Copyright 2023-Present Algolia // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package oktaApiAuth // Computed with: /* echo -n | openssl s_client -connect example.oktapreview.com:443 2>/dev/null |\ openssl x509 -noout -pubkey |\ openssl rsa -pubin -outform der 2>/dev/null |\ openssl dgst -sha256 -binary | base64 */ // used in api_test.go, oktaApiAuth_test.go, utils_test.go var pin []string = []string{"SE4qe2vdD9tAegPwO79rMnZyhHvqj3i5g1c2HkyGUNE="} // used in api_test.go, oktaApiAuth_test.go, utils_test.go type authRequest struct { path string payload map[string]string httpStatus int jsonResponseFile string } 07070100000032000081A4000000000000000000000001663BA04300001002000000000000000000000000000000000000003100000000openvpn-auth-okta-2.8.2/pkg/oktaApiAuth/utils.go// SPDX-FileCopyrightText: 2023-Present Algolia // // SPDX-License-Identifier: MPL-2.0 // // Copyright 2023-Present Algolia // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package oktaApiAuth import ( "encoding/json" "errors" "fmt" "net/http" "slices" "strings" "github.com/go-playground/validator/v10" "github.com/phuslu/log" ) // Checks that the user belongs to the allowed groups list provided in the conf func (auth *OktaApiAuth) checkAllowedGroups() error { log.Trace().Msg("oktaApiAuth.checkAllowedGroups()") // https://developer.okta.com/docs/reference/api/users/#request-parameters-8 if auth.ApiConfig.AllowedGroups != "" { validate := validator.New(validator.WithRequiredStructEnabled()) code, apiRes, err := auth.oktaReq(http.MethodGet, fmt.Sprintf("/users/%s/groups", auth.UserConfig.Username), nil) if err != nil { return err } if code != 200 && code != 202 { var authResErr ErrorResponse if err = json.Unmarshal(apiRes, &authResErr); err == nil { if err = validate.Struct(authResErr); err == nil { log.Error().Msgf("error fetching user's group list: %s", authResErr.Summary) } } return errors.New("invalid HTTP status code") } var groupRes []OktaGroup if err = json.Unmarshal(apiRes, &groupRes); err != nil { log.Error().Msgf("Error unmarshaling Okta API response: %s", err) return err } var groups = OktaGroups{Groups: groupRes} if err = validate.Struct(groups); err != nil { log.Error().Msgf("Error unmarshaling Okta API response: %s", err) return errors.New("invalid group list return by API") } var aGroups []string = strings.Split(auth.ApiConfig.AllowedGroups, ",") for _, uGroup := range groupRes { gName := uGroup.Profile.Name if slices.Contains(aGroups, gName) { log.Debug().Msgf("is a member of AllowedGroup %s", gName) return nil } } return errors.New("Not mmember of an AllowedGroup") } return nil } // Parse the pre authentication api response and create 2 factor lists: // one for the TOTP factors and one for the Push factors func (auth *OktaApiAuth) getUserFactors(preAuthRes PreAuthResponse) (factorsTOTP []AuthFactor, factorsPush []AuthFactor) { log.Trace().Msg("oktaApiAuth.getUserFactors()") for _, f := range preAuthRes.Embedded.Factors { if f.Type == "token:software:totp" { if auth.UserConfig.Passcode != "" { factorsTOTP = append(factorsTOTP, f) } } else if f.Type == "push" { factorsPush = append(factorsPush, f) } else { log.Debug().Msgf("unsupported factortype: %s, skipping", f.Type) } } return } func (auth *OktaApiAuth) preChecks() (PreAuthResponse, error) { log.Trace().Msg("oktaApiAuth.preChecks()") if err := auth.checkAllowedGroups(); err != nil { log.Error().Msgf("allowed group verification error: %s", err) return PreAuthResponse{}, err } code, apiRes, err := auth.preAuth() if err != nil { log.Error().Msgf("Error connecting to the Okta API: %s", err) return PreAuthResponse{}, err } validate := validator.New(validator.WithRequiredStructEnabled()) if code != 200 && code != 202 { if code == 429 { log.Warn().Msg("pre-authentication failed: rate limited") return PreAuthResponse{}, errors.New("pre-authentication rate limited") } var preAuthResErr ErrorResponse if err = json.Unmarshal(apiRes, &preAuthResErr); err == nil { if err = validate.Struct(preAuthResErr); err == nil { log.Warn().Msgf("pre-authentication failed: %s", preAuthResErr.Summary) return PreAuthResponse{}, errors.New("pre-authentication failed") } } } var preAuthRes PreAuthResponse if err = json.Unmarshal(apiRes, &preAuthRes); err != nil { log.Error().Msgf("Error unmarshaling Okta API response: %s", err) return PreAuthResponse{}, err } if err = validate.Struct(preAuthRes); err != nil { log.Error().Msgf("Error unmarshaling Okta API response: %s", err) return PreAuthResponse{}, err } return preAuthRes, nil } 07070100000033000081A4000000000000000000000001663BA043000011E8000000000000000000000000000000000000003600000000openvpn-auth-okta-2.8.2/pkg/oktaApiAuth/utils_test.go// SPDX-FileCopyrightText: 2023-Present Algolia // // SPDX-License-Identifier: MPL-2.0 // // Copyright 2023-Present Algolia // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package oktaApiAuth import ( "fmt" "net/http" "testing" "github.com/stretchr/testify/assert" "gopkg.in/h2non/gock.v1" ) type allowedGroupsTest struct { testName string requests []authRequest allowedGroups string token string err error } func TestCheckAllowedGroups(t *testing.T) { defer gock.Off() // Uncomment the following line to see HTTP requests intercepted by gock //gock.Observe(gock.DumpRequest) tests := []allowedGroupsTest{ { "no response - failure", []authRequest{ { fmt.Sprintf("/api/v1/users/%s/groups", username), map[string]string{"username": "TEST", "password": "TEST"}, http.StatusOK, "groups_invalid_json.json", }, }, "test1, test2", "FAKE_TOKEN", fmt.Errorf("Get \"https://example.oktapreview.com/api/v1/users/dade.murphy@example.com/groups\": gock: cannot match any request"), }, { "invalid json response - failure", []authRequest{ { fmt.Sprintf("/api/v1/users/%s/groups", username), map[string]string{"username": username, "password": password}, http.StatusOK, "groups_invalid_json.json", }, }, "test1, test2", token, fmt.Errorf("invalid character '-' in numeric literal"), }, { "invalid HTTP status code - failure", []authRequest{ { fmt.Sprintf("/api/v1/users/%s/groups", username), map[string]string{"username": username, "password": password}, http.StatusInternalServerError, "groups_invalid_json.json", }, }, "test1, test2", token, fmt.Errorf("invalid HTTP status code"), }, { "invalid token - failure", []authRequest{ { fmt.Sprintf("/api/v1/users/%s/groups", username), map[string]string{"username": username, "password": password}, http.StatusUnauthorized, "groups_invalid_token.json", }, }, "test1, test2", token, fmt.Errorf("invalid HTTP status code"), }, { "missing group name - failure", []authRequest{ { fmt.Sprintf("/api/v1/users/%s/groups", username), map[string]string{"username": username, "password": password}, http.StatusOK, "groups_no_name.json", }, }, "Cloud App Users, test2", token, fmt.Errorf("invalid group list return by API"), }, { "Member of AllowedGroups - success", []authRequest{ { fmt.Sprintf("/api/v1/users/%s/groups", username), map[string]string{"username": username, "password": password}, http.StatusOK, "groups.json", }, }, "Cloud App Users, test2", token, nil, }, { "Not member of AllowedGroups - failure", []authRequest{ { fmt.Sprintf("/api/v1/users/%s/groups", username), map[string]string{"username": username, "password": password}, http.StatusOK, "groups.json", }, }, "test1, test2", token, fmt.Errorf("Not mmember of an AllowedGroup"), }, } for _, test := range tests { t.Run(test.testName, func(t *testing.T) { gock.Clean() gock.Flush() apiCfg := &OktaAPIConfig{ Url: oktaEndpoint, Token: test.token, UsernameSuffix: "algolia.com", AssertPin: pin, MFARequired: false, AllowUntrustedUsers: true, MFAPushMaxRetries: 20, MFAPushDelaySeconds: 3, AllowedGroups: test.allowedGroups, } userCfg := &OktaUserConfig{ Username: username, Password: password, Passcode: "", ClientIp: ip, } for _, req := range test.requests { reqponseFile := fmt.Sprintf("../../testing/fixtures/oktaApi/%s", req.jsonResponseFile) l := gock.New(oktaEndpoint) l = l.Get(req.path). MatchHeader("Authorization", fmt.Sprintf("SSWS %s", token)). MatchHeader("X-Forwarded-For", ip). MatchType("json") l.Reply(req.httpStatus). File(reqponseFile) } a := New() assert.NotNil(t, a) a.ApiConfig = apiCfg a.UserConfig = userCfg err := a.InitPool() assert.Nil(t, err) gock.InterceptClient(a.pool) // Lets ensure we wont reach the real okta API gock.DisableNetworking() err = a.checkAllowedGroups() if test.err == nil { assert.Nil(t, err) } else { assert.Equal(t, test.err.Error(), err.Error()) } }) } } 07070100000034000041ED000000000000000000000002663BA04300000000000000000000000000000000000000000000002600000000openvpn-auth-okta-2.8.2/pkg/validator07070100000035000081A4000000000000000000000001663BA04300000CAF000000000000000000000000000000000000003000000000openvpn-auth-okta-2.8.2/pkg/validator/config.go// SPDX-FileCopyrightText: 2023-Present Algolia // // SPDX-License-Identifier: MPL-2.0 // // Copyright 2023-Present Algolia // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package validator import ( "errors" "gopkg.in/ini.v1" "os" "strings" "github.com/phuslu/log" ) var ( cfgDefaultPaths = [5]string{ "/etc/okta-auth-validator/api.ini", "/etc/openvpn/okta_openvpn.ini", "/etc/okta_openvpn.ini", "api.ini", "okta_openvpn.ini", } pinsetDefaultPaths = [5]string{ "/etc/okta-auth-validator/pinset.cfg", "/etc/openvpn/okta_pinset.cfg", "/etc/okta_pinset.cfg", "pinset.cfg", "okta_pinset.cfg", } ) // Read the ini file containing the API config func (validator *OktaOpenVPNValidator) readConfigFile() error { log.Trace().Msg("validator.readConfigFile()") var cfgPaths []string if validator.configFile == "" { for _, v := range cfgDefaultPaths { cfgPaths = append(cfgPaths, v) } } else { cfgPaths = append(cfgPaths, validator.configFile) } for _, cfgFile := range cfgPaths { info, err := os.Stat(cfgFile) if err != nil { continue } if info.IsDir() { continue } // should never fail as err would be not nil only if cfgFile is not a string (or a []byte, a Reader) cfg, err := ini.Load(cfgFile) if err != nil { log.Error().Msgf("Error loading ini file \"%s\": %s", cfgFile, err) return err } log.DefaultLogger.Level = log.ParseLevel( cfg.Section("General").Key("LogLevel").In( log.DefaultLogger.Level.String(), []string{"TRACE", "DEBUG", "INFO", "WARN", "WARNING", "ERROR"})) apiConfig := validator.api.ApiConfig if err := cfg.Section("OktaAPI").StrictMapTo(apiConfig); err != nil { log.Error().Msgf("Error parsing ini file \"%s\": %s", cfgFile, err) return err } if apiConfig.Url == "" || apiConfig.Token == "" { log.Error().Msgf("Missing Url or Token parameter in \"%s\"", cfgFile) return errors.New("Missing param Url or Token") } validator.configFile = cfgFile return nil } log.Error().Msgf("No ini file found in %v", cfgPaths) return errors.New("No ini file found") } // Read all allowed pubkey fingerprints for the API server from pinset file func (validator *OktaOpenVPNValidator) loadPinset() error { log.Trace().Msg("validator.loadPinset()") var pinsetPaths []string if validator.pinsetFile == "" { for _, v := range pinsetDefaultPaths { pinsetPaths = append(pinsetPaths, v) } } else { pinsetPaths = append(pinsetPaths, validator.pinsetFile) } for _, pinsetFile := range pinsetPaths { info, err := os.Stat(pinsetFile) if err != nil { continue } if info.IsDir() { continue } pinset, err := os.ReadFile(pinsetFile) if err != nil { log.Error().Msgf("Can not read pinset config file \"%s\": %s", pinsetFile, err) return err } pinsetArray := strings.Split(string(pinset), "\n") cleanPinset := removeComments(removeEmptyStrings(pinsetArray)) validator.api.ApiConfig.AssertPin = cleanPinset validator.pinsetFile = pinsetFile return nil } return errors.New("No pinset file found") } 07070100000036000081A4000000000000000000000001663BA04300000CB0000000000000000000000000000000000000003500000000openvpn-auth-okta-2.8.2/pkg/validator/config_test.go// SPDX-FileCopyrightText: 2023-Present Algolia // // SPDX-License-Identifier: MPL-2.0 // // Copyright 2023-Present Algolia // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package validator import ( "fmt" "os" "slices" "testing" "github.com/stretchr/testify/assert" "gopkg.in/algolia/openvpn-auth-okta.v2/pkg/oktaApiAuth" ) // used in TestReadConfigFile, TestLoadPinset type testCfgFile struct { testName string path string link string err error } func TestParsePassword(t *testing.T) { t.Run("Parse password with passcode", func(t *testing.T) { setEnv(setupEnv) v := New() _ = v.loadEnvVars(nil) v.api.UserConfig.Password = "password123456" unsetEnv(setupEnv) v.parsePassword() assert.Equal(t, "password", v.api.UserConfig.Password) assert.Equal(t, "123456", v.api.UserConfig.Passcode) }) } func TestReadConfigFile(t *testing.T) { tests := []testCfgFile{ { "Valid config file - success", "../../testing/fixtures/validator/valid.ini", "", nil, }, { "Valid config file link - success", "", "../../testing/fixtures/validator/valid.ini", nil, }, { "Invalid config file - failure", "../../testing/fixtures/validator/invalid.ini", "", fmt.Errorf("Missing param Url or Token"), }, { "Invalid 2 config file - failure", "../../testing/fixtures/validator/invalid2.ini", "", fmt.Errorf("key-value delimiter not found: UsernameSuffix\n"), }, { "Missing config file - failure", "MISSING", "", fmt.Errorf("No ini file found"), }, { "Config file is a dir - failure", "../../testing/fixtures/validator/", "", fmt.Errorf("No ini file found"), }, } for _, test := range tests { t.Run(test.testName, func(t *testing.T) { v := New() v.configFile = test.path if test.path == "" { _ = os.Symlink(test.link, "api.ini") } err := v.readConfigFile() if test.path == "" { _ = os.Remove("api.ini") } if test.err == nil { assert.Nil(t, err) } else { assert.Equal(t, test.err.Error(), err.Error()) } }) } } func TestLoadPinset(t *testing.T) { tests := []testCfgFile{ { "Valid pinset file - success", "../../testing/fixtures/validator/valid.cfg", "", nil, }, { "Valid pinset link - success", "", "../../testing/fixtures/validator/valid.cfg", nil, }, { "Missing pinset file - failure", "MISSING", "", fmt.Errorf("No pinset file found"), }, { "Pinset file is a dir - failure", "../../testing/fixtures/validator/", "", fmt.Errorf("No pinset file found"), }, } for _, test := range tests { t.Run(test.testName, func(t *testing.T) { v := New() v.api = oktaApiAuth.New() v.pinsetFile = test.path if test.path == "" { _ = os.Symlink(test.link, "pinset.cfg") } err := v.loadPinset() if test.path == "" { _ = os.Remove("pinset.cfg") } if test.err == nil { assert.Nil(t, err) assert.True(t, slices.Contains(v.api.ApiConfig.AssertPin, pin)) } else { assert.Equal(t, test.err.Error(), err.Error()) } }) } } 07070100000037000081A4000000000000000000000001663BA04300001074000000000000000000000000000000000000003100000000openvpn-auth-okta-2.8.2/pkg/validator/loading.go// SPDX-FileCopyrightText: 2023-Present Algolia // // SPDX-License-Identifier: MPL-2.0 // // Copyright 2023-Present Algolia // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package validator import ( "errors" "fmt" "os" "strings" "github.com/phuslu/log" ) // PluginEnv represents the information passed to the validator when it's running as // `Shared Object Plugin` type PluginEnv struct { // ControlFile is the path to the OpenVPN auth control file // where the authentication result is written ControlFile string // The OpenVPN client ip address, used as `X-Forwarded-For` payload attribute // to the Okta API ClientIp string // The CN of the SSL certificate presented by the OpenVPN client CommonName string // The client username submitted during OpenVPN authentication Username string // The client password submitted during OpenVPN authentication Password string } // Get user credentials from the OpenVPN via-file func (validator *OktaOpenVPNValidator) loadViaFile(path string) error { log.Trace().Msg("validator.loadViaFile()") if _, err := os.Stat(path); err != nil { log.Error().Msgf("OpenVPN via-file \"%s\" does not exists", path) return err } viaFileBuf, err := os.ReadFile(path) if err != nil { log.Error().Msgf("Can not read OpenVPN via-file \"%s\": %s", path, err) return err } viaFileInfos := strings.Split(string(viaFileBuf), "\n") viaFileInfos = removeEmptyStrings(viaFileInfos) if len(viaFileInfos) < 2 { log.Error().Msgf("Invalid OpenVPN via-file \"%s\" content", path) return errors.New("Invalid via-file") } username := viaFileInfos[0] password := viaFileInfos[1] if !checkUsernameFormat(username) { log.Error().Msg("Username or CN invalid format") return errors.New("Invalid CN or username format") } apiConfig := validator.api.ApiConfig validator.usernameTrusted = true if apiConfig.UsernameSuffix != "" && !strings.Contains(username, "@") { username = fmt.Sprintf("%s@%s", username, apiConfig.UsernameSuffix) } userConfig := validator.api.UserConfig userConfig.Username = username userConfig.Password = password return nil } // Get user credentials and info from the environment set by OpenVPN func (validator *OktaOpenVPNValidator) loadEnvVars(pluginEnv *PluginEnv) error { log.Trace().Msg("validator.loadEnvVars()") if pluginEnv == nil { pluginEnv = &PluginEnv{ Username: os.Getenv("username"), CommonName: os.Getenv("common_name"), Password: os.Getenv("password"), // TODO: use the local public ip as fallback ClientIp: getEnv("untrusted_ip", ""), ControlFile: os.Getenv("auth_control_file"), } } validator.controlFile = pluginEnv.ControlFile if validator.controlFile == "" { log.Warn().Msg("No control file found, if using a deferred plugin auth will stall and fail.") } // if the username comes from a certificate and AllowUntrustedUsers is false: // user is trusted // otherwise BE CAREFUL, username from OpenVPN credentials will be used ! apiConfig := validator.api.ApiConfig if pluginEnv.CommonName != "" && !apiConfig.AllowUntrustedUsers { validator.usernameTrusted = true pluginEnv.Username = pluginEnv.CommonName } // if username is empty, there is an issue somewhere if pluginEnv.Username == "" { log.Error().Msg("No username or CN provided") return errors.New("No CN or username") } if pluginEnv.Password == "" { log.Error().Msg("No password provided") return errors.New("No password") } if !checkUsernameFormat(pluginEnv.Username) { log.Error().Msg("Username or CN invalid format") return errors.New("Invalid CN or username format") } if apiConfig.AllowUntrustedUsers { validator.usernameTrusted = true } if apiConfig.UsernameSuffix != "" && !strings.Contains(pluginEnv.Username, "@") { pluginEnv.Username = fmt.Sprintf("%s@%s", pluginEnv.Username, apiConfig.UsernameSuffix) } userConfig := validator.api.UserConfig userConfig.Username = pluginEnv.Username userConfig.Password = pluginEnv.Password userConfig.ClientIp = pluginEnv.ClientIp return nil } 07070100000038000081A4000000000000000000000001663BA04300001596000000000000000000000000000000000000003600000000openvpn-auth-okta-2.8.2/pkg/validator/loading_test.go// SPDX-FileCopyrightText: 2023-Present Algolia // // SPDX-License-Identifier: MPL-2.0 // // Copyright 2023-Present Algolia // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package validator import ( "fmt" "os" "testing" "github.com/stretchr/testify/assert" "gopkg.in/algolia/openvpn-auth-okta.v2/pkg/oktaApiAuth" ) const pin string = "SE4qe2vdD9tAegPwO79rMnZyhHvqj3i5g1c2HkyGUNE=" type testViaFile struct { testName string path string usernameSuffix string expectedUsername string expectedPassword string err error } type testEnvVar struct { testName string usernameSuffix string allowUntrustedUsers bool expectedTrusted bool expectedUsername string env map[string]string err error } func TestLoadViaFile(t *testing.T) { tests := []testViaFile{ { "Valid via file with suffix - success", "../../testing/fixtures/validator/valid_viafile.cfg", "example.com", "dade.murphy@example.com", "password", nil, }, { "Valid via file without suffix - success", "../../testing/fixtures/validator/valid_viafile.cfg", "", "dade.murphy", "password", nil, }, { "Invalid via file - failure", "../../testing/fixtures/validator/invalid_viafile.cfg", "", "dade.murphy", "password", fmt.Errorf("Invalid via-file"), }, { "Invalid username in via file - failure", "../../testing/fixtures/validator/invalid_username_viafile.cfg", "", "dade.murphy*", "password", fmt.Errorf("Invalid CN or username format"), }, { "Missing via file - failure", "MISSING", "", "dade.murphy", "password", fmt.Errorf("stat MISSING: no such file or directory"), }, { "Via file is a dir - failure", "../../testing/fixtures/validator/", "", "dade.murphy", "password", fmt.Errorf("read ../../testing/fixtures/validator/: is a directory"), }, } for _, test := range tests { t.Run(test.testName, func(t *testing.T) { v := New() v.api = oktaApiAuth.New() v.api.ApiConfig.UsernameSuffix = test.usernameSuffix err := v.loadViaFile(test.path) if test.err == nil { assert.Nil(t, err) assert.NotNil(t, v.api.UserConfig) assert.Equal(t, test.expectedUsername, v.api.UserConfig.Username) assert.Equal(t, test.expectedPassword, v.api.UserConfig.Password) } else { assert.Equal(t, test.err.Error(), err.Error()) } }) } } func setEnv(e map[string]string) { for k, v := range e { os.Setenv(k, v) } } func unsetEnv(e map[string]string) { for k := range e { os.Unsetenv(k) } } func TestLoadEnvVars(t *testing.T) { tests := []testEnvVar{ { "Test username/allowUntrustedUsers/usernameSuffix - succes", "example.com", true, true, "dade.murphy@example.com", map[string]string{ "username": "dade.murphy", "common_name": "", "password": "password", "untrusted_ip": "1.2.3.4", }, nil, }, { "Test username/no password - failure", "example.com", true, false, "dade.murphy@example.com", map[string]string{ "username": "dade.murphy", "common_name": "", "password": "", "untrusted_ip": "1.2.3.4", }, fmt.Errorf("No password"), }, { "Test username/!allowUntrustedUsers/usernameSuffix - success", "example.com", false, false, "dade.murphy@example.com", map[string]string{ "username": "dade.murphy", "common_name": "", "password": "password", "untrusted_ip": "1.2.3.4", }, nil, }, { "Test common_name/!allowUntrustedUsers/usernameSuffix - success", "example.com", false, true, "dade.murphy2@example.com", map[string]string{ "username": "dade.murphy", "common_name": "dade.murphy2", "password": "password", "untrusted_ip": "1.2.3.4", }, nil, }, { "Test username/common_name/allowUntrustedUsers/usernameSuffix - success", "example.com", true, true, "dade.murphy@example.com", map[string]string{ "username": "dade.murphy", "common_name": "dade.murphy2", "password": "password", "untrusted_ip": "1.2.3.4", }, nil, }, { "Test empty username/common_name - failure", "example.com", false, false, "dade.murphy@example.com", map[string]string{ "username": "", "common_name": "", "password": "password", "untrusted_ip": "1.2.3.4", }, fmt.Errorf("No CN or username"), }, { "Test invalid username/common_name - failure", "example.com", false, false, "dade.murphy@example.com", map[string]string{ "username": "dade.murphy*", "common_name": "", "password": "password", "untrusted_ip": "1.2.3.4", }, fmt.Errorf("Invalid CN or username format"), }, } for _, test := range tests { t.Run(test.testName, func(t *testing.T) { setEnv(test.env) v := New() v.api.ApiConfig.UsernameSuffix = test.usernameSuffix v.api.ApiConfig.AllowUntrustedUsers = test.allowUntrustedUsers err := v.loadEnvVars(nil) unsetEnv(test.env) assert.Equal(t, test.expectedTrusted, v.usernameTrusted) if test.err == nil { assert.Nil(t, err) assert.Equal(t, test.expectedUsername, v.api.UserConfig.Username) } else { assert.Equal(t, test.err.Error(), err.Error()) } }) } } 07070100000039000081A4000000000000000000000001663BA0430000119F000000000000000000000000000000000000002F00000000openvpn-auth-okta-2.8.2/pkg/validator/utils.go// SPDX-FileCopyrightText: 2023-Present Algolia // // SPDX-License-Identifier: MPL-2.0 // // Copyright 2023-Present Algolia // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package validator import ( "errors" "fmt" "io" "os" "path/filepath" "regexp" "strconv" "strings" "time" "github.com/phuslu/log" ) const passcodeLen int = 6 // Parse the password looking for an TOTP func (validator *OktaOpenVPNValidator) parsePassword() { log.Trace().Msg("validator.parsePassword()") // If the password provided by the user is longer than a OTP (6 cars) // and the last 6 caracters are digits // then extract the user password (first) and the OTP userConfig := validator.api.UserConfig if len(userConfig.Password) > passcodeLen { last := userConfig.Password[len(userConfig.Password)-passcodeLen:] if _, err := strconv.Atoi(last); err == nil { userConfig.Passcode = last userConfig.Password = userConfig.Password[:len(userConfig.Password)-passcodeLen] } else { log.Debug().Msgf("no TOTP found in password") } } } // Validate the OpenVPN control file and its directory permissions func (validator *OktaOpenVPNValidator) checkControlFilePerm() error { log.Trace().Msg("validator.checkControlFilePerm()") if validator.controlFile == "" { return errors.New("Unknow control file") } if !checkNotWritable(validator.controlFile) { log.Error().Msgf("Refusing to authenticate. The file \"%s\" must not be writable by non-owners.", validator.controlFile) return errors.New("control file writable by non-owners") } dirName := filepath.Dir(validator.controlFile) if !checkNotWritable(dirName) { log.Error().Msgf("Refusing to authenticate. The directory containing the file \"%s\" must not be writable by non-owners.", validator.controlFile) return errors.New("control file dir writable by non-owners") } return nil } // get an env var by its name, returns the fallback if not found func getEnv(key, fallback string) string { if value, ok := os.LookupEnv(key); ok && value != "" { return value } return fallback } // check that username respects OpenVPN recomandation func checkUsernameFormat(name string) bool { log.Trace().Msg("validator.checkUsernameFormat()") /* OpenVPN doc says: To protect against a client passing a maliciously formed username or password string, the username string must consist only of these characters: alphanumeric, underbar ('_'), dash ('-'), dot ('.'), or at ('@'). */ match, _ := regexp.MatchString(`^([[:alnum:]]|[_\-\.@])*$`, name) return match } // Check that path is not group or other writable func checkNotWritable(path string) bool { sIWGRP := 0b000010000 // Group write permissions sIWOTH := 0b000000010 // Other write permissions fileInfo, err := os.Stat(path) if err != nil { return false } fileMode := fileInfo.Mode().Perm() if int(fileMode)&sIWGRP == sIWGRP || int(fileMode)&sIWOTH == sIWOTH { return false } return true } // remove all empty strings from string slice func removeEmptyStrings(s []string) []string { var r []string for _, str := range s { if str != "" { r = append(r, str) } } return r } // remove all comments from string slice func removeComments(s []string) []string { var r []string reg, _ := regexp.Compile(`^[[:blank:]]*#`) for _, str := range s { if match := reg.MatchString(`^[[:blank:]]*#`); !match { r = append(r, str) } } return r } // Update the log formatter to include the username func (validator *OktaOpenVPNValidator) setLogUser() { log.DefaultLogger.Writer.(*log.ConsoleWriter).Formatter = func(w io.Writer, a *log.FormatterArgs) (int, error) { return fmt.Fprintf( w, "%s [okta-auth-validator:%s](%s): [%s] %s\n", a.Time, validator.sessionId, strings.ToUpper(a.Level), validator.api.UserConfig.Username, a.Message) } } // Initialize the default logger (with the requested level) func (validator *OktaOpenVPNValidator) initLogFormatter(level log.Level) { log.DefaultLogger = log.Logger{ Level: level, Caller: 1, TimeFormat: time.ANSIC, Writer: &log.ConsoleWriter{ ColorOutput: false, Formatter: func(w io.Writer, a *log.FormatterArgs) (int, error) { return fmt.Fprintf( w, "%s [okta-auth-validator:%s](%s): %s\n", a.Time, validator.sessionId, strings.ToUpper(a.Level), a.Message) }, }, } } 0707010000003A000081A4000000000000000000000001663BA04300000B95000000000000000000000000000000000000003400000000openvpn-auth-okta-2.8.2/pkg/validator/utils_test.go// SPDX-FileCopyrightText: 2023-Present Algolia // // SPDX-License-Identifier: MPL-2.0 // // Copyright 2023-Present Algolia // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package validator import ( "fmt" "io/fs" "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" ) type usernameTest struct { testName string username string res bool } type testControlFile struct { testName string path string mode fs.FileMode err error } func TestCheckUsernameFormat(t *testing.T) { tests := []usernameTest{ { "Valid username - success", "dade.murphy@example.com", true, }, { "Invalid username - failure", "dade.*murphy/", false, }, } for _, test := range tests { t.Run(test.testName, func(t *testing.T) { res := checkUsernameFormat(test.username) assert.Equal(t, test.res, res) }) } } func TestCheckNotWritable(t *testing.T) { t.Run("File does not exist - false", func(t *testing.T) { res := checkNotWritable("MISSING") assert.False(t, res) }) } func TestGetEnv(t *testing.T) { t.Run("Env var does not exist - falback", func(t *testing.T) { res := getEnv("THIS_ENV_VER_DOES_NOT_EXIST", "value") assert.Equal(t, res, "value") }) t.Run("Env var is empty - falback", func(t *testing.T) { _ = os.Setenv("THIS_ENV_VAR_IS_EMPTY", "") res := getEnv("THIS_ENV_VAR_IS_EMPTY", "value") _ = os.Unsetenv("THIS_ENV_VAR_IS_EMPTY") assert.Equal(t, res, "value") }) } func TestCheckControlFilePerm(t *testing.T) { tests := []testControlFile{ { "Test empty control file path - failure", "", 0600, fmt.Errorf("Unknow control file"), }, { "Test valid control file permissions - success", "../../testing/fixtures/validator/valid_control_file", 0600, nil, }, { "Test invalid control file permissions - success", "../../testing/fixtures/validator/invalid_control_file", 0660, fmt.Errorf("control file writable by non-owners"), }, { "Test invalid control file dir permissions - success", "../../testing/fixtures/validator/invalid_ctrlfile_dir_perm/ctrl", 0600, fmt.Errorf("control file dir writable by non-owners"), }, } for _, test := range tests { t.Run(test.testName, func(t *testing.T) { v := New() if test.path != "" { v.controlFile = test.path _, _ = os.Create(test.path) defer func() { _ = os.Remove(test.path) }() // This is crapy but git does not group write bit ... if dirName := filepath.Base(filepath.Dir(test.path)); dirName == "invalid_ctrlfile_dir_perm" { _ = os.Chmod(filepath.Dir(test.path), 0770) } _ = os.Chmod(test.path, test.mode) } err := v.checkControlFilePerm() if test.err == nil { assert.Nil(t, err) } else { assert.Equal(t, test.err.Error(), err.Error()) } }) } } 0707010000003B000081A4000000000000000000000001663BA04300001066000000000000000000000000000000000000003300000000openvpn-auth-okta-2.8.2/pkg/validator/validator.go// SPDX-FileCopyrightText: 2023-Present Algolia // // SPDX-License-Identifier: MPL-2.0 // // Copyright 2023-Present Algolia // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package validator import ( "errors" "os" "slices" "github.com/google/uuid" "github.com/phuslu/log" "gopkg.in/algolia/openvpn-auth-okta.v2/pkg/oktaApiAuth" ) type OktaApiAuth = oktaApiAuth.OktaApiAuth type OktaOpenVPNValidator struct { configFile string pinsetFile string usernameTrusted bool isUserValid bool controlFile string sessionId string api *OktaApiAuth } // Returns a validator: // if no args is provided LogLevel will be INFO // if a arg is provided and in ["TRACE","DEBUG","INFO","WARN","WARNING","ERROR"] use it as LogLevel func New(args ...string) *OktaOpenVPNValidator { api := oktaApiAuth.New() luuid := uuid.NewString() defaultLevel := "INFO" if len(args) > 0 { if slices.Contains([]string{"TRACE", "DEBUG", "INFO", "WARN", "WARNING", "ERROR"}, args[0]) { defaultLevel = args[0] } } v := &OktaOpenVPNValidator{ usernameTrusted: false, isUserValid: false, controlFile: "", configFile: "", sessionId: luuid, api: api, } v.initLogFormatter(log.ParseLevel(defaultLevel)) return v } // Setup the validator depending on the way it's invoked func (validator *OktaOpenVPNValidator) Setup(deferred bool, args []string, pluginEnv *PluginEnv) bool { log.Trace().Msg("validator.Setup()") if err := validator.readConfigFile(); err != nil { log.Error().Msg("ReadConfigFile failure") if deferred { /* * if invoked as a deferred plugin, we should always exit 0 and write result * in the control file. * here the validator control may not have been yet set, force it */ validator.controlFile = os.Getenv("auth_control_file") validator.WriteControlFile() } return false } if !deferred { // We're running in "Script Plugins" mode with "via-env" method // see "--auth-user-pass-verify cmd method" in // https://openvpn.net/community-resources/reference-manual-for-openvpn-2-4/ if len(args) > 0 { // via-file" method if err := validator.loadViaFile(args[0]); err != nil { log.Error().Msg("LoadViaFile failure") return false } } else { // "via-env" method if err := validator.loadEnvVars(nil); err != nil { log.Error().Msg("LoadEnvVars failure") return false } } } else { // We're running in "Shared Object Plugin" mode // see https://openvpn.net/community-resources/using-alternative-authentication-methods/ if err := validator.loadEnvVars(pluginEnv); err != nil { log.Error().Msg("LoadEnvVars (deferred) failure") validator.WriteControlFile() return false } } if err := validator.loadPinset(); err != nil { log.Error().Msg("LoadPinset failure") if deferred { validator.WriteControlFile() } return false } validator.parsePassword() if err := validator.api.InitPool(); err != nil { log.Error().Msg("Initpool failure") return false } validator.setLogUser() return true } // Authenticate the user against Okta API func (validator *OktaOpenVPNValidator) Authenticate() error { log.Trace().Msg("validator.Authenticate()") if !validator.usernameTrusted { log.Warn().Msgf("is not trusted - failing") return errors.New("User not trusted") } if err := validator.api.Auth(); err != nil { return errors.New("Authentication failed") } validator.isUserValid = true return nil } // Write the authentication result in the OpenVPN control file (only used in deferred mode) func (validator *OktaOpenVPNValidator) WriteControlFile() { log.Trace().Msg("validator.WriteControlFile()") if err := validator.checkControlFilePerm(); err != nil { return } valToWrite := []byte("0") if validator.isUserValid { valToWrite = []byte("1") } if err := os.WriteFile(validator.controlFile, valToWrite, 0600); err != nil { log.Error().Msgf("Failed to write to OpenVPN control file \"%s\": %s", validator.controlFile, err) } } 0707010000003C000081A4000000000000000000000001663BA04300001E34000000000000000000000000000000000000003800000000openvpn-auth-okta-2.8.2/pkg/validator/validator_test.go// SPDX-FileCopyrightText: 2023-Present Algolia // // SPDX-License-Identifier: MPL-2.0 // // Copyright 2023-Present Algolia // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package validator import ( "fmt" "io/fs" "net/http" "os" "testing" "github.com/stretchr/testify/assert" "gopkg.in/h2non/gock.v1" ) const ( // used in TestWriteControlFile controlFile string = "../../testing/fixtures/validator/control_file" oktaEndpoint string = "https://example.oktapreview.com" token string = "12345" ) var setupEnv = map[string]string{ "username": "dade.murphy", "common_name": "", "password": "password", "untrusted_ip": "1.2.3.4", "auth_control_file": controlFile, } type testWriteFile struct { testName string userValid bool expected string } type testSetup struct { testName string cfgFile string pinsetFile string deferred bool env map[string]string args []string ret bool } type authRequest struct { path string payload map[string]string httpStatus int jsonResponseFile string } type testAuthenticate struct { testName string cfgFile string pinsetFile string userTrusted bool requests []authRequest ret bool err error } func TestAuthenticate(t *testing.T) { defer gock.Off() //gock.Observe(gock.DumpRequest) tests := []testAuthenticate{ { "Untrusted user - false", "../../testing/fixtures/validator/valid.ini", "../../testing/fixtures/validator/valid.cfg", false, nil, false, fmt.Errorf("User not trusted"), }, { "Valid user - true", "../../testing/fixtures/validator/valid.ini", "../../testing/fixtures/validator/valid.cfg", true, []authRequest{ { "/api/v1/authn", map[string]string{ "username": fmt.Sprintf("%s@example.com", setupEnv["username"]), "password": setupEnv["password"], }, http.StatusOK, "preauth_success_without_mfa.json", }, }, true, nil, }, { "Invalid user - true", "../../testing/fixtures/validator/valid.ini", "../../testing/fixtures/validator/valid.cfg", true, []authRequest{ { "/api/v1/authn", map[string]string{ "username": fmt.Sprintf("%s@example.com", setupEnv["username"]), "password": setupEnv["password"], }, http.StatusUnauthorized, "preauth_invalid_token.json", }, }, false, fmt.Errorf("Authentication failed"), }, } for _, test := range tests { t.Run(test.testName, func(t *testing.T) { gock.Clean() gock.Flush() for _, req := range test.requests { reqponseFile := fmt.Sprintf("../../testing/fixtures/oktaApi/%s", req.jsonResponseFile) l := gock.New(oktaEndpoint) l = l.Post(req.path). MatchHeader("Authorization", fmt.Sprintf("SSWS %s", token)). MatchHeader("X-Forwarded-For", setupEnv["untrusted_ip"]). MatchType("json"). JSON(req.payload) l.Reply(req.httpStatus). File(reqponseFile) } setEnv(setupEnv) v := New() v.configFile = test.cfgFile v.pinsetFile = test.pinsetFile err := v.Setup(true, nil, nil) unsetEnv(setupEnv) assert.True(t, err) v.usernameTrusted = test.userTrusted v.api.ApiConfig.MFARequired = false gock.InterceptClient(v.api.Pool()) gock.DisableNetworking() err2 := v.Authenticate() assert.Equal(t, test.ret, v.isUserValid) if test.err == nil { assert.Nil(t, err2) } else { assert.Equal(t, test.err.Error(), err2.Error()) } }) } } func TestSetup(t *testing.T) { tests := []testSetup{ { "Invalid url in config file / deferred - false", "../../testing/fixtures/validator/invalid_url.ini", "../../testing/fixtures/validator/valid.cfg", true, setupEnv, nil, false, }, { "Invalid config file / deferred - false", "../../testing/fixtures/validator/invalid.ini", "../../testing/fixtures/validator/valid.cfg", true, setupEnv, nil, false, }, { "Valid config file / valid env / deferred - true", "../../testing/fixtures/validator/valid.ini", "../../testing/fixtures/validator/valid.cfg", true, setupEnv, nil, true, }, { "Invalid env / deferred - false", "../../testing/fixtures/validator/valid.ini", "../../testing/fixtures/validator/valid.cfg", true, map[string]string{"auth_control_file": controlFile}, nil, false, }, { "Invalid pinset / deferred - false", "../../testing/fixtures/validator/valid.ini", "MISSING", true, setupEnv, nil, false, }, { "Invalid config file / via-env - false", "../../testing/fixtures/validator/invalid.ini", "../../testing/fixtures/validator/valid.cfg", false, setupEnv, nil, false, }, { "Valid config file / valid env / via-env - true", "../../testing/fixtures/validator/valid.ini", "../../testing/fixtures/validator/valid.cfg", false, setupEnv, nil, true, }, { "Invalid env / via-env - false", "../../testing/fixtures/validator/valid.ini", "../../testing/fixtures/validator/valid.cfg", false, map[string]string{"auth_control_file": controlFile}, nil, false, }, { "Invalid pinset / via-env - false", "../../testing/fixtures/validator/valid.ini", "MISSING", true, setupEnv, nil, false, }, { "Invalid config file / via-file - false", "../../testing/fixtures/validator/invalid.ini", "../../testing/fixtures/validator/valid.cfg", false, nil, []string{"../../testing/fixtures/validator/valid_viafile.cfg"}, false, }, { "Valid config file / valid via-file / via-env - true", "../../testing/fixtures/validator/valid.ini", "../../testing/fixtures/validator/valid.cfg", false, nil, []string{"../../testing/fixtures/validator/valid_viafile.cfg"}, true, }, { "Invalid via-file / via-file - false", "../../testing/fixtures/validator/valid.ini", "../../testing/fixtures/validator/valid.cfg", false, nil, []string{"../../testing/fixtures/validator/invalid_viafile.cfg"}, false, }, { "Invalid pinset / via-env - false", "../../testing/fixtures/validator/valid.ini", "MISSING", true, nil, []string{"../../testing/fixtures/validator/valid_viafile.cfg"}, false, }, } _, _ = os.Create(controlFile) defer func() { _ = os.Remove(controlFile) }() for _, test := range tests { t.Run(test.testName, func(t *testing.T) { setEnv(test.env) v := New("INFO") v.configFile = test.cfgFile v.pinsetFile = test.pinsetFile ret := v.Setup(test.deferred, test.args, nil) unsetEnv(test.env) assert.Equal(t, test.ret, ret) }) } } func TestWriteControlFile(t *testing.T) { tests := []testWriteFile{ { "Test valid user - success", true, "1", }, { "Test invalid user - success", false, "0", }, { "Test non writable control file - success", false, "", }, } var mode fs.FileMode for _, test := range tests { _, _ = os.Create(controlFile) defer func() { _ = os.Remove(controlFile) }() if test.expected == "" { mode = 0660 } else { mode = 0600 } _ = os.Chmod(controlFile, mode) t.Run(test.testName, func(t *testing.T) { v := New() v.controlFile = controlFile v.isUserValid = test.userValid v.WriteControlFile() ctrlValue, _ := os.ReadFile(controlFile) if test.expected == "" { i, _ := os.Stat(controlFile) size := i.Size() assert.Equal(t, size, int64(0)) } else { assert.Equal(t, test.expected, string(ctrlValue)) } }) } } 0707010000003D000041ED000000000000000000000003663BA04300000000000000000000000000000000000000000000002000000000openvpn-auth-okta-2.8.2/testing0707010000003E000041ED000000000000000000000005663BA04300000000000000000000000000000000000000000000002900000000openvpn-auth-okta-2.8.2/testing/fixtures0707010000003F000041ED000000000000000000000002663BA04300000000000000000000000000000000000000000000003100000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi07070100000040000081A4000000000000000000000001663BA04300000107000000000000000000000000000000000000004800000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/auth_invalid_totp.json{ "errorCode": "E0000068", "errorSummary": "Invalid Passcode/Answer", "errorLink": "E0000068", "errorId": "oaei_IfXcpnTHit_YEKGInpFw", "errorCauses": [ { "errorSummary": "Your passcode doesn't match our records. Please try again." } ] } 07070100000041000081A4000000000000000000000001663BA043000000A5000000000000000000000000000000000000004F00000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/auth_invalid_totp_no_sum.json{ "errorCode": "E0000068", "errorSummary": "Invalid Passcode/Answer", "errorLink": "E0000068", "errorId": "oaei_IfXcpnTHit_YEKGInpFw", "errorCauses": [] } 07070100000042000081A4000000000000000000000001663BA043000001B8000000000000000000000000000000000000004A00000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/auth_missing_status.json{ "expiresAt": "2015-11-03T10:15:57.000Z", "sessionToken": "00Fpzf4en68pCXTsMjcX8JPMctzN2Wiw4LDOBL_9xx", "_embedded": { "user": { "id": "00ub0oNGTSWTBKOLGLNR", "passwordChanged": "2015-09-08T20:14:45.000Z", "profile": { "login": "dade.murphy@example.com", "firstName": "Dade", "lastName": "Murphy", "locale": "en_US", "timeZone": "America/Los_Angeles" } } } } 07070100000043000081A4000000000000000000000001663BA04300000604000000000000000000000000000000000000004900000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/auth_rejected_push.json{ "stateToken": "007ucIX7PATyn94hsHfOLVaXAmOBkKHWnOOLG43bsb", "expiresAt": "2015-11-03T10:15:57.000Z", "status": "MFA_CHALLENGE", "factorResult": "REJECTED", "_embedded": { "user": { "id": "00ub0oNGTSWTBKOLGLNR", "passwordChanged": "2015-09-08T20:14:45.000Z", "profile": { "login": "dade.murphy@example.com", "firstName": "Dade", "lastName": "Murphy", "locale": "en_US", "timeZone": "America/Los_Angeles" } }, "factors": { "id": "opfh52xcuft3J4uZc0g3", "factorType": "push", "provider": "OKTA", "profile": { "deviceType": "SmartPhone_IPhone", "name": "Gibson", "platform": "IOS", "version": "9.0" } } }, "_links": { "next": { "name": "verify", "href": "https://{yourOktaDomain}/api/v1/authn/factors/opfh52xcuft3J4uZc0g3/verify", "hints": { "allow": [ "POST" ] } }, "cancel": { "href": "https://{yourOktaDomain}/api/v1/authn/cancel", "hints": { "allow": [ "POST" ] } }, "prev": { "href": "https://{yourOktaDomain}/api/v1/authn/previous", "hints": { "allow": [ "POST" ] } }, "resend": [ { "name": "push", "href": "https://{yourOktaDomain}/api/v1/authn/factors/opfh52xcuft3J4uZc0g3/verify/resend", "hints": { "allow": [ "POST" ] } } ] } } 07070100000044000081A4000000000000000000000001663BA043000001CF000000000000000000000000000000000000004300000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/auth_success.json{ "expiresAt": "2015-11-03T10:15:57.000Z", "status": "SUCCESS", "sessionToken": "00Fpzf4en68pCXTsMjcX8JPMctzN2Wiw4LDOBL_9xx", "_embedded": { "user": { "id": "00ub0oNGTSWTBKOLGLNR", "passwordChanged": "2015-09-08T20:14:45.000Z", "profile": { "login": "dade.murphy@example.com", "firstName": "Dade", "lastName": "Murphy", "locale": "en_US", "timeZone": "America/Los_Angeles" } } } } 07070100000045000081A4000000000000000000000001663BA04300000601000000000000000000000000000000000000004300000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/auth_waiting.json{ "stateToken": "007ucIX7PATyn94hsHfOLVaXAmOBkKHWnOOLG43bsb", "expiresAt": "2015-11-03T10:15:57.000Z", "status": "MFA_CHALLENGE", "factorResult": "WAITING", "_embedded": { "user": { "id": "00ub0oNGTSWTBKOLGLNR", "passwordChanged": "2015-09-08T20:14:45.000Z", "profile": { "login": "dade.murphy@example.com", "firstName": "Dade", "lastName": "Murphy", "locale": "en_US", "timeZone": "America/Los_Angeles" } }, "factors": { "id": "opf3hkfocI4JTLAju0g4", "factorType": "push", "provider": "OKTA", "profile": { "deviceType": "SmartPhone_IPhone", "name": "Gibson", "platform": "IOS", "version": "9.0" } } }, "_links": { "next": { "name": "poll", "href": "https://{yourOktaDomain}/api/v1/authn/factors/opfh52xcuft3J4uZc0g3/verify", "hints": { "allow": [ "POST" ] } }, "cancel": { "href": "https://{yourOktaDomain}/api/v1/authn/cancel", "hints": { "allow": [ "POST" ] } }, "prev": { "href": "https://{yourOktaDomain}/api/v1/authn/previous", "hints": { "allow": [ "POST" ] } }, "resend": [ { "name": "push", "href": "https://{yourOktaDomain}/api/v1/authn/factors/opfh52xcuft3J4uZc0g3/verify/resend", "hints": { "allow": [ "POST" ] } } ] } } 07070100000046000081A4000000000000000000000001663BA04300000003000000000000000000000000000000000000003C00000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/empty.json{} 07070100000047000081A4000000000000000000000001663BA04300000121000000000000000000000000000000000000003D00000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/groups.json[ { "id": "0gabcd1234", "profile": { "name": "Cloud App Users", "description": "Users can access cloud apps" } }, { "id": "0gefgh5678", "profile": { "name": "Internal App Users", "description": "Users can access internal apps" } } ] 07070100000048000081A4000000000000000000000001663BA04300000006000000000000000000000000000000000000004A00000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/groups_invalid_json.json[---] 07070100000049000081A4000000000000000000000001663BA0430000009F000000000000000000000000000000000000004B00000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/groups_invalid_token.json{ "errorCode":"E0000011", "errorSummary":"Invalid token provided", "errorLink":"E0000011", "errorId":"oaeO9pxfrcRRXGiwsYpzFgOLw", "errorCauses":[] } 0707010000004A000081A4000000000000000000000001663BA04300000100000000000000000000000000000000000000004500000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/groups_no_name.json[ { "id": "0gabcd1234", "profile": { "description": "Users can access cloud apps" } }, { "id": "0gefgh5678", "profile": { "name": "Internal App Users", "description": "Users can access internal apps" } } ] 0707010000004B000081A4000000000000000000000001663BA04300000006000000000000000000000000000000000000003E00000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/invalid.json{---} 0707010000004C000081A4000000000000000000000001663BA04300000718000000000000000000000000000000000000005300000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/preauth_2_push_mfa_providers.json{ "stateToken": "007ucIX7PATyn94hsHfOLVaXAmOBkKHWnOOLG43bsb", "expiresAt": "2015-11-03T10:15:57.000Z", "status": "MFA_REQUIRED", "_embedded": { "user": { "id": "00ub0oNGTSWTBKOLGLNR", "passwordChanged": "2015-09-08T20:14:45.000Z", "profile": { "login": "dade.murphy@example.com", "firstName": "Dade", "lastName": "Murphy", "locale": "en_US", "timeZone": "America/Los_Angeles" } }, "factors": [ { "id": "rejected", "factorType": "push", "provider": "DUO", "profile": { "credentialId": "dade.murphy@example.com", "deviceType": "SmartPhone_IPhone", "name": "Gibson", "platform": "IOS", "version": "9.0" }, "_links": { "verify": { "href": "https://{yourOktaDomain}/api/v1/authn/factors/rejected/verify", "hints": { "allow": [ "POST" ] } } } }, { "id": "opf3hkfocI4JTLAju0g4", "factorType": "push", "provider": "OKTA", "profile": { "credentialId": "dade.murphy@example.com", "deviceType": "SmartPhone_IPhone", "name": "Gibson", "platform": "IOS", "version": "9.0" }, "_links": { "verify": { "href": "https://{yourOktaDomain}/api/v1/authn/factors/opf3hkfocI4JTLAju0g4/verify", "hints": { "allow": [ "POST" ] } } } } ] }, "_links": { "cancel": { "href": "https://{yourOktaDomain}/api/v1/authn/cancel", "hints": { "allow": [ "POST" ] } } } } 0707010000004D000081A4000000000000000000000001663BA04300000635000000000000000000000000000000000000004F00000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/preauth_2_totp_providers.json{ "stateToken": "007ucIX7PATyn94hsHfOLVaXAmOBkKHWnOOLG43bsb", "expiresAt": "2015-11-03T10:15:57.000Z", "status": "MFA_REQUIRED", "_embedded": { "user": { "id": "00ub0oNGTSWTBKOLGLNR", "passwordChanged": "2015-09-08T20:14:45.000Z", "profile": { "login": "dade.murphy@example.com", "firstName": "Dade", "lastName": "Murphy", "locale": "en_US", "timeZone": "America/Los_Angeles" } }, "factors": [ { "id": "rejected", "factorType": "token:software:totp", "provider": "GOOGLE", "profile": { "credentialId": "dade.murphy@example.com" }, "_links": { "verify": { "href": "https://{yourOktaDomain}/api/v1/authn/factors/rejected/verify", "hints": { "allow": [ "POST" ] } } } }, { "id": "ostfm3hPNYSOIOIVTQWY", "factorType": "token:software:totp", "provider": "OKTA", "profile": { "credentialId": "dade.murphy@example.com" }, "_links": { "verify": { "href": "https://{yourOktaDomain}/api/v1/authn/factors/ostfm3hPNYSOIOIVTQWY/verify", "hints": { "allow": [ "POST" ] } } } } ] }, "_links": { "cancel": { "href": "https://{yourOktaDomain}/api/v1/authn/cancel", "hints": { "allow": [ "POST" ] } } } } 0707010000004E000081A4000000000000000000000001663BA043000000A3000000000000000000000000000000000000005200000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/preauth_invalid_credentials.json{ "errorCode": "E0000004", "errorSummary": "Authentication failed", "errorLink": "E0000004", "errorId": "oaeuHRrvMnuRga5UzpKIOhKpQ", "errorCauses": [] } 0707010000004F000081A4000000000000000000000001663BA043000000A4000000000000000000000000000000000000004C00000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/preauth_invalid_token.json{ "errorCode": "E0000011", "errorSummary": "Invalid token provided", "errorLink": "E0000011", "errorId": "oaeQg7PqAWRQOWFuezuAoNVFQ", "errorCauses": [] } 07070100000050000081A4000000000000000000000001663BA043000000EA000000000000000000000000000000000000004800000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/preauth_lockedout.json{ "status": "LOCKED_OUT", "_links": { "next": { "name": "unlock", "href": "https://{yourOktaDomain}/api/v1/authn/recovery/unlock", "hints": { "allow": [ "POST" ] } } } } 07070100000051000081A4000000000000000000000001663BA04300000D2E000000000000000000000000000000000000004900000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/preauth_mfa_enroll.json{ "stateToken": "007ucIX7PATyn94hsHfOLVaXAmOBkKHWnOOLG43bsb", "expiresAt": "2017-03-29T21:49:09.000Z", "status": "MFA_ENROLL", "_embedded": { "user": { "id": "00ub0oNGTSWTBKOLGLNR", "passwordChanged": "2017-03-29T21:37:25.000Z", "profile": { "login": "dade.murphy@example.com", "firstName": "Dade", "lastName": "Murphy", "locale": "en_US", "timeZone": "America/Los_Angeles" } }, "factors": [ { "factorType": "question", "provider": "OKTA", "vendorName": "OKTA", "_links": { "questions": { "href": "https://{yourOktaDomain}/api/v1/users/00u1nehnZ6qp4Qy8G0g4/factors/questions", "hints": { "allow": [ "GET" ] } }, "enroll": { "href": "https://{yourOktaDomain}/api/v1/authn/factors", "hints": { "allow": [ "POST" ] } } }, "status": "NOT_SETUP", "enrollment": "OPTIONAL" }, { "factorType": "token:software:totp", "provider": "OKTA", "vendorName": "OKTA", "_links": { "enroll": { "href": "https://{yourOktaDomain}/api/v1/authn/factors", "hints": { "allow": [ "POST" ] } } }, "status": "NOT_SETUP", "enrollment": "OPTIONAL" }, { "factorType": "token:software:totp", "provider": "GOOGLE", "vendorName": "GOOGLE", "_links": { "enroll": { "href": "https://{yourOktaDomain}/api/v1/authn/factors", "hints": { "allow": [ "POST" ] } } }, "status": "NOT_SETUP", "enrollment": "OPTIONAL" }, { "factorType": "sms", "provider": "OKTA", "vendorName": "OKTA", "_links": { "enroll": { "href": "https://{yourOktaDomain}/api/v1/authn/factors", "hints": { "allow": [ "POST" ] } } }, "status": "NOT_SETUP", "enrollment": "OPTIONAL" }, { "factorType": "push", "provider": "OKTA", "vendorName": "OKTA", "_links": { "enroll": { "href": "https://{yourOktaDomain}/api/v1/authn/factors", "hints": { "allow": [ "POST" ] } } }, "status": "NOT_SETUP", "enrollment": "OPTIONAL" }, { "factorType": "token:hardware", "provider": "YUBICO", "vendorName": "YUBICO", "_links": { "enroll": { "href": "https://{yourOktaDomain}/api/v1/authn/factors", "hints": { "allow": [ "POST" ] } } }, "status": "NOT_SETUP", "enrollment": "OPTIONAL" } ] }, "_links": { "cancel": { "href": "https://{yourOktaDomain}/api/v1/authn/cancel", "hints": { "allow": [ "POST" ] } } } } 07070100000052000081A4000000000000000000000001663BA043000006BE000000000000000000000000000000000000005100000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/preauth_mfa_required_multi.json{ "stateToken": "007ucIX7PATyn94hsHfOLVaXAmOBkKHWnOOLG43bsb", "expiresAt": "2015-11-03T10:15:57.000Z", "status": "MFA_REQUIRED", "_embedded": { "user": { "id": "00ub0oNGTSWTBKOLGLNR", "passwordChanged": "2015-09-08T20:14:45.000Z", "profile": { "login": "dade.murphy@example.com", "firstName": "Dade", "lastName": "Murphy", "locale": "en_US", "timeZone": "America/Los_Angeles" } }, "factors": [ { "id": "opf3hkfocI4JTLAju0g4", "factorType": "push", "provider": "OKTA", "profile": { "credentialId": "dade.murphy@example.com", "deviceType": "SmartPhone_IPhone", "name": "Gibson", "platform": "IOS", "version": "9.0" }, "_links": { "verify": { "href": "https://{yourOktaDomain}/api/v1/authn/factors/opf3hkfocI4JTLAju0g4/verify", "hints": { "allow": [ "POST" ] } } } }, { "id": "ostfm3hPNYSOIOIVTQWY", "factorType": "token:software:totp", "provider": "OKTA", "profile": { "credentialId": "dade.murphy@example.com" }, "_links": { "verify": { "href": "https://{yourOktaDomain}/api/v1/authn/factors/ostfm3hPNYSOIOIVTQWY/verify", "hints": { "allow": [ "POST" ] } } } } ] }, "_links": { "cancel": { "href": "https://{yourOktaDomain}/api/v1/authn/cancel", "hints": { "allow": [ "POST" ] } } } } 07070100000053000081A4000000000000000000000001663BA043000004C8000000000000000000000000000000000000004D00000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/preauth_missing_status.json{ "stateToken": "007ucIX7PATyn94hsHfOLVaXAmOBkKHWnOOLG43bsb", "expiresAt": "2015-11-03T10:15:57.000Z", "_embedded": { "user": { "id": "00ub0oNGTSWTBKOLGLNR", "passwordChanged": "2015-09-08T20:14:45.000Z", "profile": { "login": "dade.murphy@example.com", "firstName": "Dade", "lastName": "Murphy", "locale": "en_US", "timeZone": "America/Los_Angeles" } }, "factors": [ { "id": "opf3hkfocI4JTLAju0g4", "factorType": "push", "provider": "OKTA", "profile": { "credentialId": "dade.murphy@example.com", "deviceType": "SmartPhone_IPhone", "name": "Gibson", "platform": "IOS", "version": "9.0" }, "_links": { "verify": { "href": "https://{yourOktaDomain}/api/v1/authn/factors/opf3hkfocI4JTLAju0g4/verify", "hints": { "allow": [ "POST" ] } } } } ] }, "_links": { "cancel": { "href": "https://{yourOktaDomain}/api/v1/authn/cancel", "hints": { "allow": [ "POST" ] } } } } 07070100000054000081A4000000000000000000000001663BA04300000280000000000000000000000000000000000000004900000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/preauth_no_factors.json{ "stateToken": "007ucIX7PATyn94hsHfOLVaXAmOBkKHWnOOLG43bsb", "expiresAt": "2015-11-03T10:15:57.000Z", "status": "MFA_REQUIRED", "_embedded": { "user": { "id": "00ub0oNGTSWTBKOLGLNR", "passwordChanged": "2015-09-08T20:14:45.000Z", "profile": { "login": "dade.murphy@example.com", "firstName": "Dade", "lastName": "Murphy", "locale": "en_US", "timeZone": "America/Los_Angeles" } } }, "_links": { "cancel": { "href": "https://{yourOktaDomain}/api/v1/authn/cancel", "hints": { "allow": [ "POST" ] } } } } 07070100000055000081A4000000000000000000000001663BA04300000405000000000000000000000000000000000000004F00000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/preauth_password_expired.json{ "stateToken": "007ucIX7PATyn94hsHfOLVaXAmOBkKHWnOOLG43bsb", "expiresAt": "2015-11-03T10:15:57.000Z", "status": "PASSWORD_EXPIRED", "_embedded": { "user": { "id": "00ub0oNGTSWTBKOLGLNR", "passwordChanged": "2015-09-08T20:14:45.000Z", "profile": { "login": "dade.murphy@example.com", "firstName": "Dade", "lastName": "Murphy", "locale": "en_US", "timeZone": "America/Los_Angeles" } }, "policy": { "complexity": { "minLength": 8, "minLowerCase": 1, "minUpperCase": 1, "minNumber": 1, "minSymbol": 0 } } }, "_links": { "next": { "name": "changePassword", "href": "https://{yourOktaDomain}/api/v1/authn/credentials/change_password", "hints": { "allow": [ "POST" ] } }, "cancel": { "href": "https://{yourOktaDomain}/api/v1/authn/cancel", "hints": { "allow": [ "POST" ] } } } } 07070100000056000081A4000000000000000000000001663BA043000004E4000000000000000000000000000000000000005000000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/preauth_push_mfa_required.json{ "stateToken": "007ucIX7PATyn94hsHfOLVaXAmOBkKHWnOOLG43bsb", "expiresAt": "2015-11-03T10:15:57.000Z", "status": "MFA_REQUIRED", "_embedded": { "user": { "id": "00ub0oNGTSWTBKOLGLNR", "passwordChanged": "2015-09-08T20:14:45.000Z", "profile": { "login": "dade.murphy@example.com", "firstName": "Dade", "lastName": "Murphy", "locale": "en_US", "timeZone": "America/Los_Angeles" } }, "factors": [ { "id": "opf3hkfocI4JTLAju0g4", "factorType": "push", "provider": "OKTA", "profile": { "credentialId": "dade.murphy@example.com", "deviceType": "SmartPhone_IPhone", "name": "Gibson", "platform": "IOS", "version": "9.0" }, "_links": { "verify": { "href": "https://{yourOktaDomain}/api/v1/authn/factors/opf3hkfocI4JTLAju0g4/verify", "hints": { "allow": [ "POST" ] } } } } ] }, "_links": { "cancel": { "href": "https://{yourOktaDomain}/api/v1/authn/cancel", "hints": { "allow": [ "POST" ] } } } } 07070100000057000081A4000000000000000000000001663BA043000000CE000000000000000000000000000000000000004900000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/preauth_rate_limit.json{ "errorCode": "E0000047", "errorSummary": "API call exceeded rate limit due to too many requests.", "errorLink": "E0000047", "errorId": "oaeWaNHfOyQSES34-a2Dw6Phw", "errorCauses": [] } 07070100000058000081A4000000000000000000000001663BA043000001CD000000000000000000000000000000000000005200000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/preauth_success_without_mfa.json{ "expiresAt": "2015-11-03T10:15:57.000Z", "status": "SUCCESS", "sessionToken": "00Fpzf4en68pCXTsMjcX8JPMctzN2Wiw4LDOBL_9pe", "_embedded": { "user": { "id": "00ub0oNGTSWTBKOLGLNR", "passwordChanged": "2015-09-08T20:14:45.000Z", "profile": { "login": "test_user@algolia.com", "firstName": "Dade", "lastName": "Murphy", "locale": "en_US", "timeZone": "America/Los_Angeles" } } } } 07070100000059000081A4000000000000000000000001663BA04300000471000000000000000000000000000000000000005000000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/preauth_totp_mfa_required.json{ "stateToken": "007ucIX7PATyn94hsHfOLVaXAmOBkKHWnOOLG43bsb", "expiresAt": "2015-11-03T10:15:57.000Z", "status": "MFA_REQUIRED", "_embedded": { "user": { "id": "00ub0oNGTSWTBKOLGLNR", "passwordChanged": "2015-09-08T20:14:45.000Z", "profile": { "login": "dade.murphy@example.com", "firstName": "Dade", "lastName": "Murphy", "locale": "en_US", "timeZone": "America/Los_Angeles" } }, "factors": [ { "id": "ostfm3hPNYSOIOIVTQWY", "factorType": "token:software:totp", "provider": "OKTA", "profile": { "credentialId": "dade.murphy@example.com" }, "_links": { "verify": { "href": "https://{yourOktaDomain}/api/v1/authn/factors/ostfm3hPNYSOIOIVTQWY/verify", "hints": { "allow": [ "POST" ] } } } } ] }, "_links": { "cancel": { "href": "https://{yourOktaDomain}/api/v1/authn/cancel", "hints": { "allow": [ "POST" ] } } } } 0707010000005A000081A4000000000000000000000001663BA043000006BF000000000000000000000000000000000000005200000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/preauth_totp_push_providers.json{ "stateToken": "007ucIX7PATyn94hsHfOLVaXAmOBkKHWnOOLG43bsb", "expiresAt": "2015-11-03T10:15:57.000Z", "status": "MFA_REQUIRED", "_embedded": { "user": { "id": "00ub0oNGTSWTBKOLGLNR", "passwordChanged": "2015-09-08T20:14:45.000Z", "profile": { "login": "dade.murphy@example.com", "firstName": "Dade", "lastName": "Murphy", "locale": "en_US", "timeZone": "America/Los_Angeles" } }, "factors": [ { "id": "ostfm3hPNYSOIOIVTQWY", "factorType": "token:software:totp", "provider": "OKTA", "profile": { "credentialId": "dade.murphy@example.com" }, "_links": { "verify": { "href": "https://{yourOktaDomain}/api/v1/authn/factors/ostfm3hPNYSOIOIVTQWY/verify", "hints": { "allow": [ "POST" ] } } } }, { "id": "opf3hkfocI4JTLAju0g4", "factorType": "push", "provider": "OKTA", "profile": { "credentialId": "dade.murphy@example.com", "deviceType": "SmartPhone_IPhone", "name": "Gibson", "platform": "IOS", "version": "9.0" }, "_links": { "verify": { "href": "https://{yourOktaDomain}/api/v1/authn/factors/opf3hkfocI4JTLAju0g4/verify", "hints": { "allow": [ "POST" ] } } } } ] }, "_links": { "cancel": { "href": "https://{yourOktaDomain}/api/v1/authn/cancel", "hints": { "allow": [ "POST" ] } } } } 0707010000005B000081A4000000000000000000000001663BA04300000463000000000000000000000000000000000000005300000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/preauth_unknown_mfa_required.json{ "stateToken": "007ucIX7PATyn94hsHfOLVaXAmOBkKHWnOOLG43bsb", "expiresAt": "2015-11-03T10:15:57.000Z", "status": "MFA_REQUIRED", "_embedded": { "user": { "id": "00ub0oNGTSWTBKOLGLNR", "passwordChanged": "2015-09-08T20:14:45.000Z", "profile": { "login": "dade.murphy@example.com", "firstName": "Dade", "lastName": "Murphy", "locale": "en_US", "timeZone": "America/Los_Angeles" } }, "factors": [ { "id": "rsalhpMQVYKHZKXZJQEW", "factorType": "token", "provider": "RSA", "profile": { "credentialId": "dade.murphy@example.com" }, "_links": { "verify": { "href": "https://{yourOktaDomain}/api/v1/authn/factors/rsalhpMQVYKHZKXZJQEW/verify", "hints": { "allow": [ "POST" ] } } } } ] }, "_links": { "cancel": { "href": "https://{yourOktaDomain}/api/v1/authn/cancel", "hints": { "allow": [ "POST" ] } } } } 0707010000005C000081A4000000000000000000000001663BA043000004E6000000000000000000000000000000000000004D00000000openvpn-auth-okta-2.8.2/testing/fixtures/oktaApi/preauth_unknown_status.json{ "stateToken": "007ucIX7PATyn94hsHfOLVaXAmOBkKHWnOOLG43bsb", "expiresAt": "2015-11-03T10:15:57.000Z", "status": "UNKNOWN_STATUS", "_embedded": { "user": { "id": "00ub0oNGTSWTBKOLGLNR", "passwordChanged": "2015-09-08T20:14:45.000Z", "profile": { "login": "dade.murphy@example.com", "firstName": "Dade", "lastName": "Murphy", "locale": "en_US", "timeZone": "America/Los_Angeles" } }, "factors": [ { "id": "opf3hkfocI4JTLAju0g4", "factorType": "push", "provider": "OKTA", "profile": { "credentialId": "dade.murphy@example.com", "deviceType": "SmartPhone_IPhone", "name": "Gibson", "platform": "IOS", "version": "9.0" }, "_links": { "verify": { "href": "https://{yourOktaDomain}/api/v1/authn/factors/opf3hkfocI4JTLAju0g4/verify", "hints": { "allow": [ "POST" ] } } } } ] }, "_links": { "cancel": { "href": "https://{yourOktaDomain}/api/v1/authn/cancel", "hints": { "allow": [ "POST" ] } } } } 0707010000005D000041ED000000000000000000000002663BA04300000000000000000000000000000000000000000000002F00000000openvpn-auth-okta-2.8.2/testing/fixtures/utils0707010000005E000081A4000000000000000000000001663BA043000004E1000000000000000000000000000000000000003A00000000openvpn-auth-okta-2.8.2/testing/fixtures/utils/server.crt-----BEGIN CERTIFICATE----- MIIDbDCCAlQCCQCkb7YNT/ACVzANBgkqhkiG9w0BAQsFADB4MQswCQYDVQQGEwJG UjEWMBQGA1UECAwNSWxlLWRlLUZyYW5jZTEOMAwGA1UEBwwFUGFyaXMxEDAOBgNV BAoMB0FsZ29saWExLzAtBgNVBAMMJm9rdGEtb3BlbnZwbi10ZXN0LXV0aWxzLWNv bm5lY3Rpb25Qb29sMB4XDTIzMTAxMDEwNTIxMloXDTMzMTAwNzEwNTIxMloweDEL MAkGA1UEBhMCRlIxFjAUBgNVBAgMDUlsZS1kZS1GcmFuY2UxDjAMBgNVBAcMBVBh cmlzMRAwDgYDVQQKDAdBbGdvbGlhMS8wLQYDVQQDDCZva3RhLW9wZW52cG4tdGVz dC11dGlscy1jb25uZWN0aW9uUG9vbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC AQoCggEBANLkeqrL0MQRWa5gB2F77ejzIUkgUi8brEDr/GZQ40lCasD9UFpaEKGp hpgguCCoumCoo/zx6gZuvBWDI06WEW3IDvjiur5b+gB3vZnnxipCO85hjwacZIlQ mLQtoxP7LyzBr5cgHSAUpwTEElWoSzmBsEHYB4GRHvdSCpcflrxavyzyf4UGpu2m zBhnoFKJgBX6X10wK+jwOtNYzl4EbGWjhuXjrApp+bbvXpUiK4/9eJaJA1dHuEzr PMI4ju4GPmskfW8xSuluHrM6rOLRa0IEhlJQlHfJBp9HaCcogVEOQWDLkdLFpHNs gtX+Uv2w/KIyL6cTF2qPaBNP8x99GR8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA xJftbpacr9UKO8tjDL9rZ2dP86Kpgn7tsK1w8s0yApC1YsBLJiWmso7V/1UTVX8P nup6acuzxekfJ3aXyW3byNYTy6havfEeOvPjRfEF+XVYKrd+JryIndcoY3PMz4bu 0ncywNcPdiKVert9R5V6uH3ow4f0vYRi9LRxQgjzC9DTR9RWvFg+Bd4J+cNkdI6q vNpqJjQXEUIKxH7dbcVngxhNfCz73aKpj8ATJIGCXs15ZFcXjy4BFc2t5TmSy//9 jDV9faM0gzt/UEWjYiD29VJQtzBmDiYR8fTd/itWIxQAHsvdmE/sZ5t/RUK+qQcs ZlcaiLwW5e47BLVZTGqgqg== -----END CERTIFICATE----- 0707010000005F000081A4000000000000000000000001663BA043000006A8000000000000000000000000000000000000003A00000000openvpn-auth-okta-2.8.2/testing/fixtures/utils/server.key-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDS5Hqqy9DEEVmu YAdhe+3o8yFJIFIvG6xA6/xmUONJQmrA/VBaWhChqYaYILggqLpgqKP88eoGbrwV gyNOlhFtyA744rq+W/oAd72Z58YqQjvOYY8GnGSJUJi0LaMT+y8swa+XIB0gFKcE xBJVqEs5gbBB2AeBkR73UgqXH5a8Wr8s8n+FBqbtpswYZ6BSiYAV+l9dMCvo8DrT WM5eBGxlo4bl46wKafm2716VIiuP/XiWiQNXR7hM6zzCOI7uBj5rJH1vMUrpbh6z Oqzi0WtCBIZSUJR3yQafR2gnKIFRDkFgy5HSxaRzbILV/lL9sPyiMi+nExdqj2gT T/MffRkfAgMBAAECggEAXjA5MQryZH8zRJQS99D3wrDDzvsmeW+skRpUkgXSXSfS sfrw5kmPYARs3ojOc5eoFf97rj8FPHY8fochP7n6csOFHFd2/AnsMEVKYvCHKtyG BXjA/jzfEgzzBpXTXdsziJPArohjOa6/oXtIjQUrA7YOWHn5cD62S9NSc4hiNCCr 5Km8fNHrvzQdVlvL4VBHofMiN9FRzsvH4+z3+Udcvt4BJLKQuJ42Zai9vMmwLGlm iDd57bhwRLoahzE8f/N6uHUjQ/StSbr8Lk0zJeg20KYxj6hLzEhmlRm+LGxjORH6 FcVeRT0oNidKphwUfogMyNSlpQFEl7N8ID4ywEfN0QKBgQDtWg3BidKjRz1MK8nX r7N+oKUSxV78piD7luJcjxsGsryfkDSAntBnxqLqwGywVqpnri3c/fQ4Mb2Yr/eR 5jr15bIHHHD8IZ/cELpaSadG3pBYWjbB++Uf9QKBVuM4N2BJAKAs+ld7NkR5sHFU mRD3EGbgA5CBuKvrSIJ+giUnzQKBgQDjdj1vgYFuJqHJBUDK4gRMsNXZRn/ZQBEd 9+cdCdBrA7W7kBN+PG60fLMRUXySCHRl7HL9lNTxkZAhD16j+/Ozv+zXQY6H/olp TIyo19zWWQUDmqQEKDSM7xTAap4vQFyw2g+Lubx/GOG1qT53J5bzAKFlNbPvH+Wo dJSCZPYAmwKBgQDc96q41KvCEK+Te2y1HUEZEUu89vzgb3VjZGwmyZ/ak4ohbupC GBhjlLzVmgFWBcktLy5JgC/eJQii3qe4L8QSax5bmnHheRhPk086gTl3M/rkFlDG NdFw9/O2IAL3si40qJ12YjYRYktLkyVfIgV2TdHImejtq9R5/g5m4pjevQKBgBvg gecImDRHx8w7OJWk3aIIiiz21vRpRa/Gkiyc5042Ri+WmMz/2xGDtu6Ibhv3rUxQ jkdF1lNE48UpfQ/b8SI2g3BeOHmyWGTvXM3UptweTN8ENNXNl6MuKfzrFDf2S2Xh U8ZsHQ32nrME6wLvdzCRAzbEPikwX5UltI3Gkd/BAoGAJfkME0UScvSEbbPHHoq+ URl19AKch6UTb61YTh46bbjgMJLz1iF6dqHIDBUYTp5jsUDiru3Ag2cq0vRHQl0y YMTZTBSBqSsvqrnib/0bDb+Rc6up0b6BQVmt2AZ+GiUtIrBDlbLLejykbxYBqp1H hPyr7sOCtuuaVuXgiBotLdw= -----END PRIVATE KEY----- 07070100000060000041ED000000000000000000000003663BA04300000000000000000000000000000000000000000000003300000000openvpn-auth-okta-2.8.2/testing/fixtures/validator07070100000061000081A4000000000000000000000001663BA04300000037000000000000000000000000000000000000003F00000000openvpn-auth-okta-2.8.2/testing/fixtures/validator/invalid.ini# This is an invalid ini file: Missing section OktaAPI 07070100000062000081A4000000000000000000000001663BA0430000004B000000000000000000000000000000000000004000000000openvpn-auth-okta-2.8.2/testing/fixtures/validator/invalid2.ini[OktaAPI] Url: https://example.oktapreview.com Token: 12345 UsernameSuffix 07070100000063000041ED000000000000000000000002663BA04300000000000000000000000000000000000000000000004D00000000openvpn-auth-okta-2.8.2/testing/fixtures/validator/invalid_ctrlfile_dir_perm07070100000064000081A4000000000000000000000001663BA04300000000000000000000000000000000000000000000005600000000openvpn-auth-okta-2.8.2/testing/fixtures/validator/invalid_ctrlfile_dir_perm/.gitkeep07070100000065000081A4000000000000000000000001663BA0430000006B000000000000000000000000000000000000004600000000openvpn-auth-okta-2.8.2/testing/fixtures/validator/invalid_pinset.cfg# This file can contain any arbitray lines # oktapreview.com jZomPEBSDXoipA9un78hKRIeN/+U4ZteRaiX8YpWfqc= 07070100000066000081A4000000000000000000000001663BA04300000084000000000000000000000000000000000000004300000000openvpn-auth-okta-2.8.2/testing/fixtures/validator/invalid_url.ini[OktaAPI] Url: INVALID Token: 12345 UsernameSuffix: example.com AllowUntrustedUsers: True MFARequired: True MFAPushDelaySeconds: 2 07070100000067000081A4000000000000000000000001663BA04300000016000000000000000000000000000000000000005000000000openvpn-auth-okta-2.8.2/testing/fixtures/validator/invalid_username_viafile.cfgdade.murphy* password 07070100000068000081A4000000000000000000000001663BA0430000000C000000000000000000000000000000000000004700000000openvpn-auth-okta-2.8.2/testing/fixtures/validator/invalid_viafile.cfgdade.murphy 07070100000069000081A4000000000000000000000001663BA04300000098000000000000000000000000000000000000003D00000000openvpn-auth-okta-2.8.2/testing/fixtures/validator/valid.cfg# This file can contain any arbitray lines # oktapreview.com jZomPEBSDXoipA9un78hKRIeN/+U4ZteRaiX8YpWfqc= SE4qe2vdD9tAegPwO79rMnZyhHvqj3i5g1c2HkyGUNE= 0707010000006A000081A4000000000000000000000001663BA0430000009B000000000000000000000000000000000000003D00000000openvpn-auth-okta-2.8.2/testing/fixtures/validator/valid.ini[OktaAPI] Url: https://example.oktapreview.com Token: 12345 UsernameSuffix: example.com AllowUntrustedUsers: True MFARequired: True MFAPushDelaySeconds: 2 0707010000006B000081A4000000000000000000000001663BA04300000015000000000000000000000000000000000000004500000000openvpn-auth-okta-2.8.2/testing/fixtures/validator/valid_viafile.cfgdade.murphy password 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!543 blocks
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor