mirror of
https://github.com/Luzifer/nginx-sso.git
synced 2024-12-20 12:51:17 +00:00
Add support for Duo MFA (#28)
Squashed commit of the following: commit f748cc99802dc2a1c2f1b805a280c56fc6cf4123 Author: Knut Ahlers <knut@ahlers.me> Date: Fri Dec 28 23:39:07 2018 +0100 Implement fetching remote IP from mutliple sources Signed-off-by: Knut Ahlers <knut@ahlers.me> commit416af9eed8
Author: Ben Edmunds <bensammy2@yahoo.co.uk> Date: Fri Dec 28 19:35:26 2018 +0000 Duo MFA, clean up & documentation commit0e511023f0
Author: Ben Edmunds <bensammy2@yahoo.co.uk> Date: Fri Dec 28 16:34:55 2018 +0000 Add support for Duo MFA closes #28 closes #24 Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
f958f16e51
commit
9e0907f494
13 changed files with 4415 additions and 0 deletions
13
Gopkg.lock
generated
13
Gopkg.lock
generated
|
@ -37,6 +37,17 @@
|
|||
revision = "3cfea5ab600ae37946be2b763b8ec2c1cf2d272d"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:6911d618517b98a7b59bf3f244ce648a29c81e2c4413f2cda9224a1d2a9b4840"
|
||||
name = "github.com/duosecurity/duo_api_golang"
|
||||
packages = [
|
||||
".",
|
||||
"authapi",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "61e0defebf22863592a27013b399685d0def12b6"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:b7eb152b61f41f0c9bcb6cf1187e1470a91d5e01d1e08f8b7bfd722578851720"
|
||||
|
@ -183,6 +194,8 @@
|
|||
"github.com/GeertJohan/yubigo",
|
||||
"github.com/Luzifer/go_helpers/str",
|
||||
"github.com/Luzifer/rconfig",
|
||||
"github.com/duosecurity/duo_api_golang",
|
||||
"github.com/duosecurity/duo_api_golang/authapi",
|
||||
"github.com/flosch/pongo2",
|
||||
"github.com/gorilla/context",
|
||||
"github.com/gorilla/sessions",
|
||||
|
|
20
README.md
20
README.md
|
@ -188,6 +188,26 @@ attributes:
|
|||
<mapping of attributes>
|
||||
```
|
||||
|
||||
#### Duo
|
||||
|
||||
This provider needs a configuration to function correctly:
|
||||
|
||||
```yaml
|
||||
mfa:
|
||||
duo:
|
||||
# Get your ikey / skey / host from https://duo.com/docs/duoweb#first-steps
|
||||
ikey: "<IKEY>"
|
||||
skey: "<SKEY>"
|
||||
host: "<API HOST>"
|
||||
user_agent: "nginx-sso"
|
||||
```
|
||||
|
||||
The corresponding expected MFA configuration is as following:
|
||||
|
||||
```yaml
|
||||
provider: duo
|
||||
```
|
||||
|
||||
#### Google Authenticator
|
||||
|
||||
The provider name here is `google` while the only supported argument at the moment is `secret`. The secret is what you need to provide to your users for them to add the config to their authenticator. (It MUST be base32 encoded!)
|
||||
|
|
|
@ -43,6 +43,12 @@ mfa:
|
|||
client_id: "12345"
|
||||
secret_key: "foobar"
|
||||
|
||||
duo:
|
||||
ikey: "IKEY"
|
||||
skey: "SKEY"
|
||||
host: "HOST"
|
||||
user_agent: "nginx-sso"
|
||||
|
||||
providers:
|
||||
# Authentication against an Atlassian Crowd directory server
|
||||
# Supports: Users, Groups
|
||||
|
@ -96,6 +102,7 @@ providers:
|
|||
# MFA configs: Username to configs mapping
|
||||
mfa:
|
||||
luzifer:
|
||||
- provider: duo
|
||||
- provider: google
|
||||
attributes:
|
||||
secret: MZXW6YTBOIFA
|
||||
|
|
131
mfa_duo.go
Normal file
131
mfa_duo.go
Normal file
|
@ -0,0 +1,131 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
duoapi "github.com/duosecurity/duo_api_golang"
|
||||
"github.com/duosecurity/duo_api_golang/authapi"
|
||||
"github.com/pkg/errors"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
mfaDuoResponseAllow = "allow"
|
||||
mfaDuoRequestTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
var mfaDuoTrustedIPHeaders = []string{"X-Forwarded-For", "X-Real-IP"}
|
||||
|
||||
func init() {
|
||||
registerMFAProvider(&mfaDuo{})
|
||||
}
|
||||
|
||||
type mfaDuo struct {
|
||||
IKey string `yaml:"ikey"`
|
||||
SKey string `yaml:"skey"`
|
||||
Host string `yaml:"host"`
|
||||
UserAgent string `yaml:"user_agent"`
|
||||
}
|
||||
|
||||
// ProviderID needs to return an unique string to identify
|
||||
// this special MFA provider
|
||||
func (m mfaDuo) ProviderID() (id string) { return "duo" }
|
||||
|
||||
// Configure loads the configuration for the Authenticator from the
|
||||
// global config.yaml file which is passed as a byte-slice.
|
||||
// If no configuration for the Authenticator is supplied the function
|
||||
// needs to return the errProviderUnconfigured
|
||||
func (m *mfaDuo) Configure(yamlSource []byte) (err error) {
|
||||
envelope := struct {
|
||||
MFA struct {
|
||||
Duo *mfaDuo `yaml:"duo"`
|
||||
} `yaml:"mfa"`
|
||||
}{}
|
||||
|
||||
if err := yaml.Unmarshal(yamlSource, &envelope); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if envelope.MFA.Duo == nil {
|
||||
return errProviderUnconfigured
|
||||
}
|
||||
|
||||
m.IKey = envelope.MFA.Duo.IKey
|
||||
m.SKey = envelope.MFA.Duo.SKey
|
||||
m.Host = envelope.MFA.Duo.Host
|
||||
m.UserAgent = envelope.MFA.Duo.UserAgent
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateMFA takes the user from the login cookie and performs a
|
||||
// validation against the provided MFA configuration for this user
|
||||
func (m mfaDuo) ValidateMFA(res http.ResponseWriter, r *http.Request, user string, mfaCfgs []mfaConfig) error {
|
||||
var keyInput string
|
||||
|
||||
// Look for mfaConfigs with own provider name
|
||||
for _, c := range mfaCfgs {
|
||||
if c.Provider != m.ProviderID() {
|
||||
continue
|
||||
}
|
||||
|
||||
remoteIP, err := m.findIP(r)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Unable to determine remote IP")
|
||||
}
|
||||
|
||||
duo := authapi.NewAuthApi(*duoapi.NewDuoApi(m.IKey, m.SKey, m.Host, m.UserAgent, duoapi.SetTimeout(mfaDuoRequestTimeout)))
|
||||
|
||||
for key, values := range r.Form {
|
||||
if strings.HasSuffix(key, mfaLoginFieldName) && len(values[0]) > 0 {
|
||||
keyInput = values[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Check if MFA token provided and fallover to push if not supplied
|
||||
var auth *authapi.AuthResult
|
||||
|
||||
if keyInput != "" {
|
||||
if auth, err = duo.Auth("passcode", authapi.AuthUsername(user), authapi.AuthPasscode(keyInput), authapi.AuthIpAddr(remoteIP)); err != nil {
|
||||
return errors.Wrap(err, "Unable to authenticate with Duo using 'passcode' method")
|
||||
}
|
||||
} else {
|
||||
if auth, err = duo.Auth("auto", authapi.AuthUsername(user), authapi.AuthDevice("auto"), authapi.AuthIpAddr(remoteIP)); err != nil {
|
||||
return errors.Wrap(err, "Unable to authenticate with Duo using 'auto' method")
|
||||
}
|
||||
}
|
||||
|
||||
if auth.Response.Result == mfaDuoResponseAllow {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Report this provider was not able to verify the MFA request
|
||||
return errNoValidUserFound
|
||||
}
|
||||
|
||||
func (m mfaDuo) findIP(r *http.Request) (string, error) {
|
||||
for _, hdr := range mfaDuoTrustedIPHeaders {
|
||||
if value := r.Header.Get(hdr); value != "" {
|
||||
return m.parseIP(value)
|
||||
}
|
||||
}
|
||||
|
||||
return m.parseIP(r.RemoteAddr)
|
||||
}
|
||||
|
||||
func (m mfaDuo) parseIP(s string) (string, error) {
|
||||
ip, _, err := net.SplitHostPort(s)
|
||||
if err == nil {
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
ip2 := net.ParseIP(s)
|
||||
if ip2 == nil {
|
||||
return "", errors.New("invalid IP")
|
||||
}
|
||||
|
||||
return ip2.String(), nil
|
||||
}
|
25
vendor/github.com/duosecurity/duo_api_golang/LICENSE
generated
vendored
Normal file
25
vendor/github.com/duosecurity/duo_api_golang/LICENSE
generated
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
Copyright (c) 2015, Duo Security, Inc.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. The name of the author may not be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
19
vendor/github.com/duosecurity/duo_api_golang/README.md
generated
vendored
Normal file
19
vendor/github.com/duosecurity/duo_api_golang/README.md
generated
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Overview
|
||||
|
||||
**duo_api_golang** - Go language bindings for the Duo APIs (both auth and admin).
|
||||
|
||||
## Duo Auth API
|
||||
|
||||
The Auth API is a low-level, RESTful API for adding strong two-factor authentication to your website or application.
|
||||
|
||||
This module's API client implementation is *complete*; corresponding methods are exported for all available endpoints.
|
||||
|
||||
For more information see the [Auth API guide](https://duo.com/docs/authapi).
|
||||
|
||||
## Duo Admin API
|
||||
|
||||
The Admin API provides programmatic access to the administrative functionality of Duo Security's two-factor authentication platform.
|
||||
|
||||
This module's API client implementation is *incomplete*; methods for fetching most entity types are exported, but methods that modify entities have (mostly) not yet been implemented. PRs welcome!
|
||||
|
||||
For more information see the [Admin API guide](https://duo.com/docs/adminapi).
|
722
vendor/github.com/duosecurity/duo_api_golang/admin/admin.go
generated
vendored
Normal file
722
vendor/github.com/duosecurity/duo_api_golang/admin/admin.go
generated
vendored
Normal file
|
@ -0,0 +1,722 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/duosecurity/duo_api_golang"
|
||||
)
|
||||
|
||||
// Client provides access to Duo's admin API.
|
||||
type Client struct {
|
||||
duoapi.DuoApi
|
||||
}
|
||||
|
||||
type ListResultMetadata struct {
|
||||
NextOffset json.Number `json:"next_offset"`
|
||||
PrevOffset json.Number `json:"prev_offset"`
|
||||
TotalObjects json.Number `json:"total_objects"`
|
||||
}
|
||||
|
||||
type ListResult struct {
|
||||
Metadata ListResultMetadata `json:"metadata"`
|
||||
}
|
||||
|
||||
func (l *ListResult) metadata() ListResultMetadata {
|
||||
return l.Metadata
|
||||
}
|
||||
|
||||
// New initializes an admin API Client struct.
|
||||
func New(base duoapi.DuoApi) *Client {
|
||||
return &Client{base}
|
||||
}
|
||||
|
||||
// User models a single user.
|
||||
type User struct {
|
||||
Alias1 *string
|
||||
Alias2 *string
|
||||
Alias3 *string
|
||||
Alias4 *string
|
||||
Created uint64
|
||||
Email string
|
||||
FirstName *string
|
||||
Groups []Group
|
||||
LastDirectorySync *uint64 `json:"last_directory_sync"`
|
||||
LastLogin *uint64 `json:"last_login"`
|
||||
LastName *string
|
||||
Notes string
|
||||
Phones []Phone
|
||||
RealName *string
|
||||
Status string
|
||||
Tokens []Token
|
||||
UserID string `json:"user_id"`
|
||||
Username string
|
||||
}
|
||||
|
||||
// Group models a group to which users may belong.
|
||||
type Group struct {
|
||||
Desc string
|
||||
GroupID string `json:"group_id"`
|
||||
MobileOTPEnabled bool `json:"mobile_otp_enabled"`
|
||||
Name string
|
||||
PushEnabled bool `json:"push_enabled"`
|
||||
SMSEnabled bool `json:"sms_enabled"`
|
||||
Status string
|
||||
VoiceEnabled bool `json:"voice_enabled"`
|
||||
}
|
||||
|
||||
// Phone models a user's phone.
|
||||
type Phone struct {
|
||||
Activated bool
|
||||
Capabilities []string
|
||||
Encrypted string
|
||||
Extension string
|
||||
Fingerprint string
|
||||
Name string
|
||||
Number string
|
||||
PhoneID string `json:"phone_id"`
|
||||
Platform string
|
||||
Postdelay string
|
||||
Predelay string
|
||||
Screenlock string
|
||||
SMSPasscodesSent bool
|
||||
Type string
|
||||
Users []User
|
||||
}
|
||||
|
||||
// Token models a hardware security token.
|
||||
type Token struct {
|
||||
TokenID string `json:"token_id"`
|
||||
Type string
|
||||
Serial string
|
||||
TOTPStep *int `json:"totp_step"`
|
||||
Users []User
|
||||
}
|
||||
|
||||
// U2FToken models a U2F security token.
|
||||
type U2FToken struct {
|
||||
DateAdded uint64 `json:"date_added"`
|
||||
RegistrationID string `json:"registration_id"`
|
||||
User *User
|
||||
}
|
||||
|
||||
// Common URL options
|
||||
|
||||
// Limit sets the optional limit parameter for an API request.
|
||||
func Limit(limit uint64) func(*url.Values) {
|
||||
return func(opts *url.Values) {
|
||||
opts.Set("limit", strconv.FormatUint(limit, 10))
|
||||
}
|
||||
}
|
||||
|
||||
// Offset sets the optional offset parameter for an API request.
|
||||
func Offset(offset uint64) func(*url.Values) {
|
||||
return func(opts *url.Values) {
|
||||
opts.Set("offset", strconv.FormatUint(offset, 10))
|
||||
}
|
||||
}
|
||||
|
||||
// User methods
|
||||
|
||||
// GetUsersUsername sets the optional username parameter for a GetUsers request.
|
||||
func GetUsersUsername(name string) func(*url.Values) {
|
||||
return func(opts *url.Values) {
|
||||
opts.Set("username", name)
|
||||
}
|
||||
}
|
||||
|
||||
// GetUsersResult models responses containing a list of users.
|
||||
type GetUsersResult struct {
|
||||
duoapi.StatResult
|
||||
ListResult
|
||||
Response []User
|
||||
}
|
||||
|
||||
func (result *GetUsersResult) getResponse() interface{} {
|
||||
return result.Response
|
||||
}
|
||||
|
||||
func (result *GetUsersResult) appendResponse(users interface{}) {
|
||||
asserted_users := users.([]User)
|
||||
result.Response = append(result.Response, asserted_users...)
|
||||
}
|
||||
|
||||
// GetUsers calls GET /admin/v1/users
|
||||
// See https://duo.com/docs/adminapi#retrieve-users
|
||||
func (c *Client) GetUsers(options ...func(*url.Values)) (*GetUsersResult, error) {
|
||||
params := url.Values{}
|
||||
for _, o := range options {
|
||||
o(¶ms)
|
||||
}
|
||||
|
||||
cb := func(params url.Values) (responsePage, error) {
|
||||
return c.retrieveUsers(params)
|
||||
}
|
||||
response, err := c.retrieveItems(params, cb)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response.(*GetUsersResult), nil
|
||||
}
|
||||
|
||||
type responsePage interface {
|
||||
metadata() ListResultMetadata
|
||||
getResponse() interface{}
|
||||
appendResponse(interface{})
|
||||
}
|
||||
|
||||
type pageFetcher func(params url.Values) (responsePage, error)
|
||||
|
||||
func (c *Client) retrieveItems(
|
||||
params url.Values,
|
||||
fetcher pageFetcher,
|
||||
) (responsePage, error) {
|
||||
if params.Get("offset") == "" {
|
||||
params.Set("offset", "0")
|
||||
}
|
||||
|
||||
if params.Get("limit") == "" {
|
||||
params.Set("limit", "100")
|
||||
accumulator, firstErr := fetcher(params)
|
||||
|
||||
if firstErr != nil {
|
||||
return nil, firstErr
|
||||
}
|
||||
|
||||
params.Set("offset", accumulator.metadata().NextOffset.String())
|
||||
for ; params.Get("offset") != "" ; {
|
||||
nextResult, err := fetcher(params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nextResult.appendResponse(accumulator.getResponse())
|
||||
accumulator = nextResult
|
||||
params.Set("offset", accumulator.metadata().NextOffset.String())
|
||||
}
|
||||
return accumulator, nil
|
||||
}
|
||||
|
||||
return fetcher(params)
|
||||
}
|
||||
|
||||
func (c *Client) retrieveUsers(params url.Values) (*GetUsersResult, error) {
|
||||
_, body, err := c.SignedCall(http.MethodGet, "/admin/v1/users", params, duoapi.UseTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &GetUsersResult{}
|
||||
err = json.Unmarshal(body, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetUser calls GET /admin/v1/users/:user_id
|
||||
// See https://duo.com/docs/adminapi#retrieve-user-by-id
|
||||
func (c *Client) GetUser(userID string) (*GetUsersResult, error) {
|
||||
path := fmt.Sprintf("/admin/v1/users/%s", userID)
|
||||
|
||||
_, body, err := c.SignedCall(http.MethodGet, path, nil, duoapi.UseTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &GetUsersResult{}
|
||||
err = json.Unmarshal(body, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetUserGroups calls GET /admin/v1/users/:user_id/groups
|
||||
// See https://duo.com/docs/adminapi#retrieve-groups-by-user-id
|
||||
func (c *Client) GetUserGroups(userID string, options ...func(*url.Values)) (*GetGroupsResult, error) {
|
||||
params := url.Values{}
|
||||
for _, o := range options {
|
||||
o(¶ms)
|
||||
}
|
||||
|
||||
cb := func(params url.Values) (responsePage, error) {
|
||||
return c.retrieveUserGroups(userID, params)
|
||||
}
|
||||
response, err := c.retrieveItems(params, cb)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response.(*GetGroupsResult), nil
|
||||
}
|
||||
|
||||
func (c *Client) retrieveUserGroups(userID string, params url.Values) (*GetGroupsResult, error) {
|
||||
path := fmt.Sprintf("/admin/v1/users/%s/groups", userID)
|
||||
|
||||
_, body, err := c.SignedCall(http.MethodGet, path, params, duoapi.UseTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &GetGroupsResult{}
|
||||
err = json.Unmarshal(body, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetUserPhones calls GET /admin/v1/users/:user_id/phones
|
||||
// See https://duo.com/docs/adminapi#retrieve-phones-by-user-id
|
||||
func (c *Client) GetUserPhones(userID string, options ...func(*url.Values)) (*GetPhonesResult, error) {
|
||||
params := url.Values{}
|
||||
for _, o := range options {
|
||||
o(¶ms)
|
||||
}
|
||||
|
||||
cb := func(params url.Values) (responsePage, error) {
|
||||
return c.retrieveUserPhones(userID, params)
|
||||
}
|
||||
response, err := c.retrieveItems(params, cb)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response.(*GetPhonesResult), nil
|
||||
}
|
||||
|
||||
func (c *Client) retrieveUserPhones(userID string, params url.Values) (*GetPhonesResult, error) {
|
||||
path := fmt.Sprintf("/admin/v1/users/%s/phones", userID)
|
||||
|
||||
_, body, err := c.SignedCall(http.MethodGet, path, params, duoapi.UseTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &GetPhonesResult{}
|
||||
err = json.Unmarshal(body, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetUserTokens calls GET /admin/v1/users/:user_id/tokens
|
||||
// See https://duo.com/docs/adminapi#retrieve-hardware-tokens-by-user-id
|
||||
func (c *Client) GetUserTokens(userID string, options ...func(*url.Values)) (*GetTokensResult, error) {
|
||||
params := url.Values{}
|
||||
for _, o := range options {
|
||||
o(¶ms)
|
||||
}
|
||||
|
||||
cb := func(params url.Values) (responsePage, error) {
|
||||
return c.retrieveUserTokens(userID, params)
|
||||
}
|
||||
response, err := c.retrieveItems(params, cb)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response.(*GetTokensResult), nil
|
||||
}
|
||||
|
||||
func (c *Client) retrieveUserTokens(userID string, params url.Values) (*GetTokensResult, error) {
|
||||
path := fmt.Sprintf("/admin/v1/users/%s/tokens", userID)
|
||||
|
||||
_, body, err := c.SignedCall(http.MethodGet, path, params, duoapi.UseTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &GetTokensResult{}
|
||||
err = json.Unmarshal(body, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// StringResult models responses containing a simple string.
|
||||
type StringResult struct {
|
||||
duoapi.StatResult
|
||||
Response string
|
||||
}
|
||||
|
||||
// AssociateUserToken calls POST /admin/v1/users/:user_id/tokens
|
||||
// See https://duo.com/docs/adminapi#associate-hardware-token-with-user
|
||||
func (c *Client) AssociateUserToken(userID, tokenID string) (*StringResult, error) {
|
||||
path := fmt.Sprintf("/admin/v1/users/%s/tokens", userID)
|
||||
|
||||
params := url.Values{}
|
||||
params.Set("token_id", tokenID)
|
||||
|
||||
_, body, err := c.SignedCall(http.MethodPost, path, params, duoapi.UseTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &StringResult{}
|
||||
err = json.Unmarshal(body, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetUserU2FTokens calls GET /admin/v1/users/:user_id/u2ftokens
|
||||
// See https://duo.com/docs/adminapi#retrieve-u2f-tokens-by-user-id
|
||||
func (c *Client) GetUserU2FTokens(userID string, options ...func(*url.Values)) (*GetU2FTokensResult, error) {
|
||||
params := url.Values{}
|
||||
for _, o := range options {
|
||||
o(¶ms)
|
||||
}
|
||||
|
||||
cb := func(params url.Values) (responsePage, error) {
|
||||
return c.retrieveUserU2FTokens(userID, params)
|
||||
}
|
||||
response, err := c.retrieveItems(params, cb)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response.(*GetU2FTokensResult), nil
|
||||
}
|
||||
|
||||
func (c *Client) retrieveUserU2FTokens(userID string, params url.Values) (*GetU2FTokensResult, error) {
|
||||
path := fmt.Sprintf("/admin/v1/users/%s/u2ftokens", userID)
|
||||
|
||||
_, body, err := c.SignedCall(http.MethodGet, path, params, duoapi.UseTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &GetU2FTokensResult{}
|
||||
err = json.Unmarshal(body, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Group methods
|
||||
|
||||
// GetGroupsResult models responses containing a list of groups.
|
||||
type GetGroupsResult struct {
|
||||
duoapi.StatResult
|
||||
ListResult
|
||||
Response []Group
|
||||
}
|
||||
|
||||
func (result *GetGroupsResult) getResponse() interface{} {
|
||||
return result.Response
|
||||
}
|
||||
|
||||
func (result *GetGroupsResult) appendResponse(groups interface{}) {
|
||||
asserted_groups := groups.([]Group)
|
||||
result.Response = append(result.Response, asserted_groups...)
|
||||
}
|
||||
|
||||
// GetGroups calls GET /admin/v1/groups
|
||||
// See https://duo.com/docs/adminapi#retrieve-groups
|
||||
func (c *Client) GetGroups(options ...func(*url.Values)) (*GetGroupsResult, error) {
|
||||
params := url.Values{}
|
||||
for _, o := range options {
|
||||
o(¶ms)
|
||||
}
|
||||
|
||||
cb := func(params url.Values) (responsePage, error) {
|
||||
return c.retrieveGroups(params)
|
||||
}
|
||||
response, err := c.retrieveItems(params, cb)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response.(*GetGroupsResult), nil
|
||||
}
|
||||
|
||||
func (c *Client) retrieveGroups(params url.Values) (*GetGroupsResult, error) {
|
||||
_, body, err := c.SignedCall(http.MethodGet, "/admin/v1/groups", params, duoapi.UseTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &GetGroupsResult{}
|
||||
err = json.Unmarshal(body, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetGroupResult models responses containing a single group.
|
||||
type GetGroupResult struct {
|
||||
duoapi.StatResult
|
||||
Response Group
|
||||
}
|
||||
|
||||
// GetGroup calls GET /admin/v2/group/:group_id
|
||||
// See https://duo.com/docs/adminapi#get-group-info
|
||||
func (c *Client) GetGroup(groupID string) (*GetGroupResult, error) {
|
||||
path := fmt.Sprintf("/admin/v2/groups/%s", groupID)
|
||||
|
||||
_, body, err := c.SignedCall(http.MethodGet, path, nil, duoapi.UseTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &GetGroupResult{}
|
||||
err = json.Unmarshal(body, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Phone methods
|
||||
|
||||
// GetPhonesNumber sets the optional number parameter for a GetPhones request.
|
||||
func GetPhonesNumber(number string) func(*url.Values) {
|
||||
return func(opts *url.Values) {
|
||||
opts.Set("number", number)
|
||||
}
|
||||
}
|
||||
|
||||
// GetPhonesExtension sets the optional extension parameter for a GetPhones request.
|
||||
func GetPhonesExtension(ext string) func(*url.Values) {
|
||||
return func(opts *url.Values) {
|
||||
opts.Set("extension", ext)
|
||||
}
|
||||
}
|
||||
|
||||
// GetPhonesResult models responses containing a list of phones.
|
||||
type GetPhonesResult struct {
|
||||
duoapi.StatResult
|
||||
ListResult
|
||||
Response []Phone
|
||||
}
|
||||
|
||||
func (result *GetPhonesResult) getResponse() interface{} {
|
||||
return result.Response
|
||||
}
|
||||
|
||||
func (result *GetPhonesResult) appendResponse(phones interface{}) {
|
||||
asserted_phones := phones.([]Phone)
|
||||
result.Response = append(result.Response, asserted_phones...)
|
||||
}
|
||||
|
||||
|
||||
// GetPhones calls GET /admin/v1/phones
|
||||
// See https://duo.com/docs/adminapi#phones
|
||||
func (c *Client) GetPhones(options ...func(*url.Values)) (*GetPhonesResult, error) {
|
||||
params := url.Values{}
|
||||
for _, o := range options {
|
||||
o(¶ms)
|
||||
}
|
||||
|
||||
cb := func(params url.Values) (responsePage, error) {
|
||||
return c.retrievePhones(params)
|
||||
}
|
||||
response, err := c.retrieveItems(params, cb)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response.(*GetPhonesResult), nil
|
||||
}
|
||||
|
||||
func (c *Client) retrievePhones(params url.Values) (*GetPhonesResult, error) {
|
||||
_, body, err := c.SignedCall(http.MethodGet, "/admin/v1/phones", params, duoapi.UseTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &GetPhonesResult{}
|
||||
err = json.Unmarshal(body, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetPhoneResult models responses containing a single phone.
|
||||
type GetPhoneResult struct {
|
||||
duoapi.StatResult
|
||||
Response Phone
|
||||
}
|
||||
|
||||
// GetPhone calls GET /admin/v1/phones/:phone_id
|
||||
// See https://duo.com/docs/adminapi#retrieve-phone-by-id
|
||||
func (c *Client) GetPhone(phoneID string) (*GetPhoneResult, error) {
|
||||
path := fmt.Sprintf("/admin/v1/phones/%s", phoneID)
|
||||
|
||||
_, body, err := c.SignedCall(http.MethodGet, path, nil, duoapi.UseTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &GetPhoneResult{}
|
||||
err = json.Unmarshal(body, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Token methods
|
||||
|
||||
// GetTokensTypeAndSerial sets the optional type and serial parameters for a GetTokens request.
|
||||
func GetTokensTypeAndSerial(typ, serial string) func(*url.Values) {
|
||||
return func(opts *url.Values) {
|
||||
opts.Set("type", typ)
|
||||
opts.Set("serial", serial)
|
||||
}
|
||||
}
|
||||
|
||||
// GetTokensResult models responses containing a list of tokens.
|
||||
type GetTokensResult struct {
|
||||
duoapi.StatResult
|
||||
ListResult
|
||||
Response []Token
|
||||
}
|
||||
|
||||
func (result *GetTokensResult) getResponse() interface{} {
|
||||
return result.Response
|
||||
}
|
||||
|
||||
func (result *GetTokensResult) appendResponse(tokens interface{}) {
|
||||
asserted_tokens := tokens.([]Token)
|
||||
result.Response = append(result.Response, asserted_tokens...)
|
||||
}
|
||||
|
||||
|
||||
// GetTokens calls GET /admin/v1/tokens
|
||||
// See https://duo.com/docs/adminapi#retrieve-hardware-tokens
|
||||
func (c *Client) GetTokens(options ...func(*url.Values)) (*GetTokensResult, error) {
|
||||
params := url.Values{}
|
||||
for _, o := range options {
|
||||
o(¶ms)
|
||||
}
|
||||
|
||||
cb := func(params url.Values) (responsePage, error) {
|
||||
return c.retrieveTokens(params)
|
||||
}
|
||||
response, err := c.retrieveItems(params, cb)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response.(*GetTokensResult), nil
|
||||
}
|
||||
|
||||
func (c *Client) retrieveTokens(params url.Values) (*GetTokensResult, error) {
|
||||
_, body, err := c.SignedCall(http.MethodGet, "/admin/v1/tokens", params, duoapi.UseTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &GetTokensResult{}
|
||||
err = json.Unmarshal(body, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetTokenResult models responses containing a single token.
|
||||
type GetTokenResult struct {
|
||||
duoapi.StatResult
|
||||
Response Token
|
||||
}
|
||||
|
||||
// GetToken calls GET /admin/v1/tokens/:token_id
|
||||
// See https://duo.com/docs/adminapi#retrieve-hardware-tokens
|
||||
func (c *Client) GetToken(tokenID string) (*GetTokenResult, error) {
|
||||
path := fmt.Sprintf("/admin/v1/tokens/%s", tokenID)
|
||||
|
||||
_, body, err := c.SignedCall(http.MethodGet, path, nil, duoapi.UseTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &GetTokenResult{}
|
||||
err = json.Unmarshal(body, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// U2F token methods
|
||||
|
||||
// GetU2FTokensResult models responses containing a list of U2F tokens.
|
||||
type GetU2FTokensResult struct {
|
||||
duoapi.StatResult
|
||||
ListResult
|
||||
Response []U2FToken
|
||||
}
|
||||
|
||||
func (result *GetU2FTokensResult) getResponse() interface{} {
|
||||
return result.Response
|
||||
}
|
||||
|
||||
func (result *GetU2FTokensResult) appendResponse(tokens interface{}) {
|
||||
asserted_tokens := tokens.([]U2FToken)
|
||||
result.Response = append(result.Response, asserted_tokens...)
|
||||
}
|
||||
|
||||
|
||||
// GetU2FTokens calls GET /admin/v1/u2ftokens
|
||||
// See https://duo.com/docs/adminapi#retrieve-u2f-tokens
|
||||
func (c *Client) GetU2FTokens(options ...func(*url.Values)) (*GetU2FTokensResult, error) {
|
||||
params := url.Values{}
|
||||
for _, o := range options {
|
||||
o(¶ms)
|
||||
}
|
||||
|
||||
cb := func(params url.Values) (responsePage, error) {
|
||||
return c.retrieveU2FTokens(params)
|
||||
}
|
||||
response, err := c.retrieveItems(params, cb)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response.(*GetU2FTokensResult), nil
|
||||
}
|
||||
|
||||
func (c *Client) retrieveU2FTokens(params url.Values) (*GetU2FTokensResult, error) {
|
||||
_, body, err := c.SignedCall(http.MethodGet, "/admin/v1/u2ftokens", params, duoapi.UseTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &GetU2FTokensResult{}
|
||||
err = json.Unmarshal(body, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetU2FToken calls GET /admin/v1/u2ftokens/:registration_id
|
||||
// See https://duo.com/docs/adminapi#retrieve-u2f-token-by-id
|
||||
func (c *Client) GetU2FToken(registrationID string) (*GetU2FTokensResult, error) {
|
||||
path := fmt.Sprintf("/admin/v1/u2ftokens/%s", registrationID)
|
||||
|
||||
_, body, err := c.SignedCall(http.MethodGet, path, nil, duoapi.UseTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &GetU2FTokensResult{}
|
||||
err = json.Unmarshal(body, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
1949
vendor/github.com/duosecurity/duo_api_golang/admin/admin_test.go
generated
vendored
Normal file
1949
vendor/github.com/duosecurity/duo_api_golang/admin/admin_test.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
381
vendor/github.com/duosecurity/duo_api_golang/authapi/authapi.go
generated
vendored
Normal file
381
vendor/github.com/duosecurity/duo_api_golang/authapi/authapi.go
generated
vendored
Normal file
|
@ -0,0 +1,381 @@
|
|||
package authapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/duosecurity/duo_api_golang"
|
||||
)
|
||||
|
||||
type AuthApi struct {
|
||||
duoapi.DuoApi
|
||||
}
|
||||
|
||||
// Build a new Duo Auth API object.
|
||||
// api is a duoapi.DuoApi object used to make the Duo Rest API calls.
|
||||
// Example: authapi.NewAuthApi(*duoapi.NewDuoApi(ikey,skey,host,userAgent,duoapi.SetTimeout(10*time.Second)))
|
||||
func NewAuthApi(api duoapi.DuoApi) *AuthApi {
|
||||
return &AuthApi{api}
|
||||
}
|
||||
|
||||
// Leaving for backwards compatibility.
|
||||
// The struct in use has been moved to the duoapi package, to be shared between the admin and authapi packages.
|
||||
type StatResult struct {
|
||||
Stat string
|
||||
Code *int32
|
||||
Message *string
|
||||
Message_Detail *string
|
||||
}
|
||||
|
||||
// Return object for the 'Ping' API call.
|
||||
type PingResult struct {
|
||||
duoapi.StatResult
|
||||
Response struct {
|
||||
Time int64
|
||||
}
|
||||
}
|
||||
|
||||
// Duo's Ping method. https://www.duosecurity.com/docs/authapi#/ping
|
||||
// This is an unsigned Duo Rest API call which returns the Duo system's time.
|
||||
// Use this method to determine whether your system time is in sync with Duo's.
|
||||
func (api *AuthApi) Ping() (*PingResult, error) {
|
||||
_, body, err := api.Call("GET", "/auth/v2/ping", nil, duoapi.UseTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := &PingResult{}
|
||||
if err = json.Unmarshal(body, ret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Return object for the 'Check' API call.
|
||||
type CheckResult struct {
|
||||
duoapi.StatResult
|
||||
Response struct {
|
||||
Time int64
|
||||
}
|
||||
}
|
||||
|
||||
// Call Duo's Check method. https://www.duosecurity.com/docs/authapi#/check
|
||||
// Check is a signed Duo API call, which returns the Duo system's time.
|
||||
// Use this method to determine whether your ikey, skey and host are correct,
|
||||
// and whether your system time is in sync with Duo's.
|
||||
func (api *AuthApi) Check() (*CheckResult, error) {
|
||||
_, body, err := api.SignedCall("GET", "/auth/v2/check", nil, duoapi.UseTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := &CheckResult{}
|
||||
if err = json.Unmarshal(body, ret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Return object for the 'Logo' API call.
|
||||
type LogoResult struct {
|
||||
duoapi.StatResult
|
||||
png *[]byte
|
||||
}
|
||||
|
||||
// Duo's Logo method. https://www.duosecurity.com/docs/authapi#/logo
|
||||
// If the API call is successful, the configured logo png is returned. Othwerwise,
|
||||
// error information is returned in the LogoResult return value.
|
||||
func (api *AuthApi) Logo() (*LogoResult, error) {
|
||||
resp, body, err := api.SignedCall("GET", "/auth/v2/logo", nil, duoapi.UseTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode == 200 {
|
||||
ret := &LogoResult{StatResult: duoapi.StatResult{Stat: "OK"},
|
||||
png: &body}
|
||||
return ret, nil
|
||||
}
|
||||
ret := &LogoResult{}
|
||||
if err = json.Unmarshal(body, ret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Optional parameter for the Enroll method.
|
||||
func EnrollUsername(username string) func(*url.Values) {
|
||||
return func(opts *url.Values) {
|
||||
opts.Set("username", username)
|
||||
}
|
||||
}
|
||||
|
||||
// Optional parameter for the Enroll method.
|
||||
func EnrollValidSeconds(secs uint64) func(*url.Values) {
|
||||
return func(opts *url.Values) {
|
||||
opts.Set("valid_secs", strconv.FormatUint(secs, 10))
|
||||
}
|
||||
}
|
||||
|
||||
// Enroll return type.
|
||||
type EnrollResult struct {
|
||||
duoapi.StatResult
|
||||
Response struct {
|
||||
Activation_Barcode string
|
||||
Activation_Code string
|
||||
Expiration int64
|
||||
User_Id string
|
||||
Username string
|
||||
}
|
||||
}
|
||||
|
||||
// Duo's Enroll method. https://www.duosecurity.com/docs/authapi#/enroll
|
||||
// Use EnrollUsername() to include the optional username parameter.
|
||||
// Use EnrollValidSeconds() to change the default validation time limit that the
|
||||
// user has to complete enrollment.
|
||||
func (api *AuthApi) Enroll(options ...func(*url.Values)) (*EnrollResult, error) {
|
||||
opts := url.Values{}
|
||||
for _, o := range options {
|
||||
o(&opts)
|
||||
}
|
||||
|
||||
_, body, err := api.SignedCall("POST", "/auth/v2/enroll", opts, duoapi.UseTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := &EnrollResult{}
|
||||
if err = json.Unmarshal(body, ret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Response is "success", "invalid" or "waiting".
|
||||
type EnrollStatusResult struct {
|
||||
duoapi.StatResult
|
||||
Response string
|
||||
}
|
||||
|
||||
// Duo's EnrollStatus method. https://www.duosecurity.com/docs/authapi#/enroll_status
|
||||
// Return the status of an outstanding Enrollment.
|
||||
func (api *AuthApi) EnrollStatus(userid string,
|
||||
activationCode string) (*EnrollStatusResult, error) {
|
||||
queryArgs := url.Values{}
|
||||
queryArgs.Set("user_id", userid)
|
||||
queryArgs.Set("activation_code", activationCode)
|
||||
|
||||
_, body, err := api.SignedCall("POST",
|
||||
"/auth/v2/enroll_status",
|
||||
queryArgs,
|
||||
duoapi.UseTimeout)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := &EnrollStatusResult{}
|
||||
if err = json.Unmarshal(body, ret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Preauth return type.
|
||||
type PreauthResult struct {
|
||||
duoapi.StatResult
|
||||
Response struct {
|
||||
Result string
|
||||
Status_Msg string
|
||||
Enroll_Portal_Url string
|
||||
Devices []struct {
|
||||
Device string
|
||||
Type string
|
||||
Name string
|
||||
Number string
|
||||
Capabilities []string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func PreauthUserId(userid string) func(*url.Values) {
|
||||
return func(opts *url.Values) {
|
||||
opts.Set("user_id", userid)
|
||||
}
|
||||
}
|
||||
|
||||
func PreauthUsername(username string) func(*url.Values) {
|
||||
return func(opts *url.Values) {
|
||||
opts.Set("username", username)
|
||||
}
|
||||
}
|
||||
|
||||
func PreauthIpAddr(ip string) func(*url.Values) {
|
||||
return func(opts *url.Values) {
|
||||
opts.Set("ipaddr", ip)
|
||||
}
|
||||
}
|
||||
|
||||
func PreauthTrustedToken(trustedtoken string) func(*url.Values) {
|
||||
return func(opts *url.Values) {
|
||||
opts.Set("trusted_device_token", trustedtoken)
|
||||
}
|
||||
}
|
||||
|
||||
// Duo's Preauth method. https://www.duosecurity.com/docs/authapi#/preauth
|
||||
// options Optional values to include in the preauth call.
|
||||
// Use PreauthUserId to specify the user_id parameter.
|
||||
// Use PreauthUsername to specify the username parameter. You must
|
||||
// specify PreauthUserId or PreauthUsername, but not both.
|
||||
// Use PreauthIpAddr to include the ipaddr parameter, the ip address
|
||||
// of the client attempting authroization.
|
||||
// Use PreauthTrustedToken to specify the trusted_device_token parameter.
|
||||
func (api *AuthApi) Preauth(options ...func(*url.Values)) (*PreauthResult, error) {
|
||||
opts := url.Values{}
|
||||
for _, o := range options {
|
||||
o(&opts)
|
||||
}
|
||||
_, body, err := api.SignedCall("POST", "/auth/v2/preauth", opts, duoapi.UseTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := &PreauthResult{}
|
||||
if err = json.Unmarshal(body, ret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func AuthUserId(userid string) func(*url.Values) {
|
||||
return func(opts *url.Values) {
|
||||
opts.Set("user_id", userid)
|
||||
}
|
||||
}
|
||||
|
||||
func AuthUsername(username string) func(*url.Values) {
|
||||
return func(opts *url.Values) {
|
||||
opts.Set("username", username)
|
||||
}
|
||||
}
|
||||
|
||||
func AuthIpAddr(ip string) func(*url.Values) {
|
||||
return func(opts *url.Values) {
|
||||
opts.Set("ipaddr", ip)
|
||||
}
|
||||
}
|
||||
|
||||
func AuthAsync() func(*url.Values) {
|
||||
return func(opts *url.Values) {
|
||||
opts.Set("async", "1")
|
||||
}
|
||||
}
|
||||
|
||||
func AuthDevice(device string) func(*url.Values) {
|
||||
return func(opts *url.Values) {
|
||||
opts.Set("device", device)
|
||||
}
|
||||
}
|
||||
|
||||
func AuthType(type_ string) func(*url.Values) {
|
||||
return func(opts *url.Values) {
|
||||
opts.Set("type", type_)
|
||||
}
|
||||
}
|
||||
|
||||
func AuthDisplayUsername(username string) func(*url.Values) {
|
||||
return func(opts *url.Values) {
|
||||
opts.Set("display_username", username)
|
||||
}
|
||||
}
|
||||
|
||||
func AuthPushinfo(pushinfo string) func(*url.Values) {
|
||||
return func(opts *url.Values) {
|
||||
opts.Set("pushinfo", pushinfo)
|
||||
}
|
||||
}
|
||||
|
||||
func AuthPasscode(passcode string) func(*url.Values) {
|
||||
return func(opts *url.Values) {
|
||||
opts.Set("passcode", passcode)
|
||||
}
|
||||
}
|
||||
|
||||
// Auth return type.
|
||||
type AuthResult struct {
|
||||
duoapi.StatResult
|
||||
Response struct {
|
||||
// Synchronous
|
||||
Result string
|
||||
Status string
|
||||
Status_Msg string
|
||||
Trusted_Device_Token string
|
||||
// Asynchronous
|
||||
Txid string
|
||||
}
|
||||
}
|
||||
|
||||
// Duo's Auth method. https://www.duosecurity.com/docs/authapi#/auth
|
||||
// Factor must be one of 'auto', 'push', 'passcode', 'sms' or 'phone'.
|
||||
// Use AuthUserId to specify the user_id.
|
||||
// Use AuthUsername to speicy the username. You must specify either AuthUserId
|
||||
// or AuthUsername, but not both.
|
||||
// Use AuthIpAddr to include the client's IP address.
|
||||
// Use AuthAsync to toggle whether the call blocks for the user's response or not.
|
||||
// If used asynchronously, get the auth status with the AuthStatus method.
|
||||
// When using factor 'push', use AuthDevice to specify the device ID to push to.
|
||||
// When using factor 'push', use AuthType to display some extra auth text to the user.
|
||||
// When using factor 'push', use AuthDisplayUsername to display some extra text
|
||||
// to the user.
|
||||
// When using factor 'push', use AuthPushInfo to include some URL-encoded key/value
|
||||
// pairs to display to the user.
|
||||
// When using factor 'passcode', use AuthPasscode to specify the passcode entered
|
||||
// by the user.
|
||||
// When using factor 'sms' or 'phone', use AuthDevice to specify which device
|
||||
// should receive the SMS or phone call.
|
||||
func (api *AuthApi) Auth(factor string, options ...func(*url.Values)) (*AuthResult, error) {
|
||||
params := url.Values{}
|
||||
for _, o := range options {
|
||||
o(¶ms)
|
||||
}
|
||||
params.Set("factor", factor)
|
||||
|
||||
var apiOps []duoapi.DuoApiOption
|
||||
if _, ok := params["async"]; ok == true {
|
||||
apiOps = append(apiOps, duoapi.UseTimeout)
|
||||
}
|
||||
|
||||
_, body, err := api.SignedCall("POST", "/auth/v2/auth", params, apiOps...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := &AuthResult{}
|
||||
if err = json.Unmarshal(body, ret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// AuthStatus return type.
|
||||
type AuthStatusResult struct {
|
||||
duoapi.StatResult
|
||||
Response struct {
|
||||
Result string
|
||||
Status string
|
||||
Status_Msg string
|
||||
Trusted_Device_Token string
|
||||
}
|
||||
}
|
||||
|
||||
// Duo's auth_status method. https://www.duosecurity.com/docs/authapi#/auth_status
|
||||
// When using the Auth call in async mode, use this method to retrieve the
|
||||
// result of the authentication attempt.
|
||||
// txid is returned by the Auth call.
|
||||
func (api *AuthApi) AuthStatus(txid string) (*AuthStatusResult, error) {
|
||||
opts := url.Values{}
|
||||
opts.Set("txid", txid)
|
||||
_, body, err := api.SignedCall("GET", "/auth/v2/auth_status", opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := &AuthStatusResult{}
|
||||
if err = json.Unmarshal(body, ret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
}
|
604
vendor/github.com/duosecurity/duo_api_golang/authapi/authapi_test.go
generated
vendored
Normal file
604
vendor/github.com/duosecurity/duo_api_golang/authapi/authapi_test.go
generated
vendored
Normal file
|
@ -0,0 +1,604 @@
|
|||
package authapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/duosecurity/duo_api_golang"
|
||||
)
|
||||
|
||||
func buildAuthApi(url string, proxy func(*http.Request) (*url.URL, error)) *AuthApi {
|
||||
ikey := "eyekey"
|
||||
skey := "esskey"
|
||||
host := strings.Split(url, "//")[1]
|
||||
userAgent := "GoTestClient"
|
||||
return NewAuthApi(*duoapi.NewDuoApi(ikey,
|
||||
skey,
|
||||
host,
|
||||
userAgent,
|
||||
duoapi.SetTimeout(1*time.Second),
|
||||
duoapi.SetInsecure(),
|
||||
duoapi.SetProxy(proxy)))
|
||||
}
|
||||
|
||||
func getBodyParams(r *http.Request) (url.Values, error) {
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
r.Body.Close()
|
||||
if err != nil {
|
||||
return url.Values{}, err
|
||||
}
|
||||
req_params, err := url.ParseQuery(string(body))
|
||||
return req_params, err
|
||||
}
|
||||
|
||||
// Timeouts are set to 1 second. Take 15 seconds to respond and verify
|
||||
// that the client times out.
|
||||
func TestTimeout(t *testing.T) {
|
||||
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(15 * time.Second)
|
||||
}))
|
||||
|
||||
duo := buildAuthApi(ts.URL, nil)
|
||||
|
||||
start := time.Now()
|
||||
_, err := duo.Ping()
|
||||
duration := time.Since(start)
|
||||
if duration.Seconds() > 2 {
|
||||
t.Errorf("Timeout took %v seconds", duration.Seconds())
|
||||
}
|
||||
if err == nil {
|
||||
t.Error("Expected timeout error.")
|
||||
}
|
||||
}
|
||||
|
||||
// Test a successful ping request / response.
|
||||
func TestPing(t *testing.T) {
|
||||
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, `
|
||||
{
|
||||
"stat": "OK",
|
||||
"response": {
|
||||
"time": 1357020061,
|
||||
"unexpected_parameter" : "blah"
|
||||
}
|
||||
}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
duo := buildAuthApi(ts.URL, nil)
|
||||
|
||||
result, err := duo.Ping()
|
||||
if err != nil {
|
||||
t.Error("Unexpected error from Ping call" + err.Error())
|
||||
}
|
||||
if result.Stat != "OK" {
|
||||
t.Error("Expected OK, but got " + result.Stat)
|
||||
}
|
||||
if result.Response.Time != 1357020061 {
|
||||
t.Errorf("Expected 1357020061, but got %d", result.Response.Time)
|
||||
}
|
||||
}
|
||||
|
||||
// Test a successful Check request / response.
|
||||
func TestCheck(t *testing.T) {
|
||||
ts := httptest.NewTLSServer(
|
||||
http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, `
|
||||
{
|
||||
"stat": "OK",
|
||||
"response": {
|
||||
"time": 1357020061
|
||||
}
|
||||
}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
duo := buildAuthApi(ts.URL, nil)
|
||||
|
||||
result, err := duo.Check()
|
||||
if err != nil {
|
||||
t.Error("Failed TestCheck: " + err.Error())
|
||||
}
|
||||
if result.Stat != "OK" {
|
||||
t.Error("Expected OK, but got " + result.Stat)
|
||||
}
|
||||
if result.Response.Time != 1357020061 {
|
||||
t.Errorf("Expected 1357020061, but got %d", result.Response.Time)
|
||||
}
|
||||
}
|
||||
|
||||
// Test a successful Check request / response through a proxy
|
||||
func TestProxy(t *testing.T) {
|
||||
// Proxy server
|
||||
ps := httptest.NewServer(
|
||||
http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "CONNECT" {
|
||||
// Proxy the connection through to the requested host.
|
||||
conn, err := net.Dial("tcp", r.URL.Host)
|
||||
if err != nil {
|
||||
t.Error("Failed to connect to " + r.URL.String() + ", " + err.Error())
|
||||
return
|
||||
}
|
||||
// Take over the request connection.
|
||||
hj, _ := w.(http.Hijacker)
|
||||
reqconn, _, err := hj.Hijack()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Tell the client that everything is going to be OK.
|
||||
reqconn.Write([]byte("HTTP/1.0 200 OK\r\n\r\n"))
|
||||
// Copy all the things.
|
||||
f := func(src, dst net.Conn) {
|
||||
defer src.Close()
|
||||
io.Copy(src, dst)
|
||||
}
|
||||
go f(conn, reqconn)
|
||||
go f(reqconn, conn)
|
||||
} else {
|
||||
t.Error("Expected CONNECT, but got " + r.Method)
|
||||
}
|
||||
}))
|
||||
defer ps.Close()
|
||||
|
||||
// Duo dummy server response.
|
||||
ts := httptest.NewTLSServer(
|
||||
http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, `
|
||||
{
|
||||
"stat": "OK",
|
||||
"response": {
|
||||
"time": 1357020061
|
||||
}
|
||||
}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
// Connect through the test proxy.
|
||||
proxy_url, err := url.Parse(ps.URL)
|
||||
duo := buildAuthApi(ts.URL, http.ProxyURL(proxy_url))
|
||||
|
||||
result, err := duo.Check()
|
||||
if err != nil {
|
||||
t.Fatal("Failed TestCheck: " + err.Error())
|
||||
}
|
||||
if result.Stat != "OK" {
|
||||
t.Error("Expected OK, but got " + result.Stat)
|
||||
}
|
||||
if result.Response.Time != 1357020061 {
|
||||
t.Errorf("Expected 1357020061, but got %d", result.Response.Time)
|
||||
}
|
||||
}
|
||||
|
||||
// Test a successful logo request / response.
|
||||
func TestLogo(t *testing.T) {
|
||||
ts := httptest.NewTLSServer(
|
||||
http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
w.Write([]byte("\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00" +
|
||||
"\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00" +
|
||||
"\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx" +
|
||||
"\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00" +
|
||||
"\x00\x00\x00IEND\xaeB`\x82"))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
duo := buildAuthApi(ts.URL, nil)
|
||||
|
||||
_, err := duo.Logo()
|
||||
if err != nil {
|
||||
t.Error("Failed TestCheck: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Test a failure logo reqeust / response.
|
||||
func TestLogoError(t *testing.T) {
|
||||
ts := httptest.NewTLSServer(
|
||||
http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
// Return a 400, as if the logo was not found.
|
||||
w.WriteHeader(400)
|
||||
fmt.Fprintln(w, `
|
||||
{
|
||||
"stat": "FAIL",
|
||||
"code": 40002,
|
||||
"message": "Logo not found",
|
||||
"message_detail": "Why u no have logo?"
|
||||
}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
duo := buildAuthApi(ts.URL, nil)
|
||||
|
||||
res, err := duo.Logo()
|
||||
if err != nil {
|
||||
t.Error("Failed TestCheck: " + err.Error())
|
||||
}
|
||||
if res.Stat != "FAIL" {
|
||||
t.Error("Expected FAIL, but got " + res.Stat)
|
||||
}
|
||||
if res.Code == nil || *res.Code != 40002 {
|
||||
t.Error("Unexpected response code.")
|
||||
}
|
||||
if res.Message == nil || *res.Message != "Logo not found" {
|
||||
t.Error("Unexpected message.")
|
||||
}
|
||||
if res.Message_Detail == nil || *res.Message_Detail != "Why u no have logo?" {
|
||||
t.Error("Unexpected message detail.")
|
||||
}
|
||||
}
|
||||
|
||||
// Test a successful enroll request / response.
|
||||
func TestEnroll(t *testing.T) {
|
||||
ts := httptest.NewTLSServer(
|
||||
http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
req_params, err := getBodyParams(r)
|
||||
if err != nil {
|
||||
t.Error("Failed to retrieve body parameters")
|
||||
}
|
||||
if req_params.Get("username") != "49c6c3097adb386048c84354d82ea63d" {
|
||||
t.Error("TestEnroll failed to set 'username' query parameter:" +
|
||||
r.RequestURI)
|
||||
}
|
||||
if req_params.Get("valid_secs") != "10" {
|
||||
t.Error("TestEnroll failed to set 'valid_secs' query parameter: " +
|
||||
r.RequestURI)
|
||||
}
|
||||
fmt.Fprintln(w, `
|
||||
{
|
||||
"stat": "OK",
|
||||
"response": {
|
||||
"activation_barcode": "https://api-eval.duosecurity.com/frame/qr?value=8LIRa5danrICkhHtkLxi-cKLu2DWzDYCmBwBHY2YzW5ZYnYaRxA",
|
||||
"activation_code": "duo://8LIRa5danrICkhHtkLxi-cKLu2DWzDYCmBwBHY2YzW5ZYnYaRxA",
|
||||
"expiration": 1357020061,
|
||||
"user_id": "DU94SWSN4ADHHJHF2HXT",
|
||||
"username": "49c6c3097adb386048c84354d82ea63d"
|
||||
}
|
||||
}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
duo := buildAuthApi(ts.URL, nil)
|
||||
|
||||
result, err := duo.Enroll(EnrollUsername("49c6c3097adb386048c84354d82ea63d"), EnrollValidSeconds(10))
|
||||
if err != nil {
|
||||
t.Error("Failed TestEnroll: " + err.Error())
|
||||
}
|
||||
if result.Stat != "OK" {
|
||||
t.Error("Expected OK, but got " + result.Stat)
|
||||
}
|
||||
if result.Response.Activation_Barcode != "https://api-eval.duosecurity.com/frame/qr?value=8LIRa5danrICkhHtkLxi-cKLu2DWzDYCmBwBHY2YzW5ZYnYaRxA" {
|
||||
t.Error("Unexpected activation_barcode: " + result.Response.Activation_Barcode)
|
||||
}
|
||||
if result.Response.Activation_Code != "duo://8LIRa5danrICkhHtkLxi-cKLu2DWzDYCmBwBHY2YzW5ZYnYaRxA" {
|
||||
t.Error("Unexpected activation code: " + result.Response.Activation_Code)
|
||||
}
|
||||
if result.Response.Expiration != 1357020061 {
|
||||
t.Errorf("Unexpected expiration time: %d", result.Response.Expiration)
|
||||
}
|
||||
if result.Response.User_Id != "DU94SWSN4ADHHJHF2HXT" {
|
||||
t.Error("Unexpected user id: " + result.Response.User_Id)
|
||||
}
|
||||
if result.Response.Username != "49c6c3097adb386048c84354d82ea63d" {
|
||||
t.Error("Unexpected username: " + result.Response.Username)
|
||||
}
|
||||
}
|
||||
|
||||
// Test a succesful enroll status request / response.
|
||||
func TestEnrollStatus(t *testing.T) {
|
||||
ts := httptest.NewTLSServer(
|
||||
http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
req_params, err := getBodyParams(r)
|
||||
if err != nil {
|
||||
t.Error("Failed to retrieve body parameters")
|
||||
}
|
||||
if req_params.Get("user_id") != "49c6c3097adb386048c84354d82ea63d" {
|
||||
t.Error("TestEnrollStatus failed to set 'user_id' query parameter:" +
|
||||
r.RequestURI)
|
||||
}
|
||||
if req_params.Get("activation_code") != "10" {
|
||||
t.Error("TestEnrollStatus failed to set 'activation_code' query parameter: " +
|
||||
r.RequestURI)
|
||||
}
|
||||
fmt.Fprintln(w, `
|
||||
{
|
||||
"stat": "OK",
|
||||
"response": "success"
|
||||
}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
duo := buildAuthApi(ts.URL, nil)
|
||||
|
||||
result, err := duo.EnrollStatus("49c6c3097adb386048c84354d82ea63d", "10")
|
||||
if err != nil {
|
||||
t.Error("Failed TestEnrollStatus: " + err.Error())
|
||||
}
|
||||
if result.Stat != "OK" {
|
||||
t.Error("Expected OK, but got " + result.Stat)
|
||||
}
|
||||
if result.Response != "success" {
|
||||
t.Error("Unexpected response: " + result.Response)
|
||||
}
|
||||
}
|
||||
|
||||
// Test a successful preauth with user id. The client doesn't enforce api requirements,
|
||||
// such as requiring only one of user id or username, but we'll cover the username
|
||||
// in another test anyway.
|
||||
func TestPreauthUserId(t *testing.T) {
|
||||
ts := httptest.NewTLSServer(
|
||||
http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
req_params, err := getBodyParams(r)
|
||||
if err != nil {
|
||||
t.Error("Failed to retrieve body parameters")
|
||||
}
|
||||
if req_params.Get("ipaddr") != "127.0.0.1" {
|
||||
t.Error("TestPreauth failed to set 'ipaddr' query parameter:" +
|
||||
r.RequestURI)
|
||||
}
|
||||
if req_params.Get("user_id") != "10" {
|
||||
t.Error("TestEnrollStatus failed to set 'user_id' query parameter: " +
|
||||
r.RequestURI)
|
||||
}
|
||||
if req_params.Get("trusted_device_token") != "l33t" {
|
||||
t.Error("TestEnrollStatus failed to set 'trusted_device_token' query parameter: " +
|
||||
r.RequestURI)
|
||||
}
|
||||
fmt.Fprintln(w, `
|
||||
{
|
||||
"stat": "OK",
|
||||
"response": {
|
||||
"result": "auth",
|
||||
"status_msg": "Account is active",
|
||||
"devices": [
|
||||
{
|
||||
"device": "DPFZRS9FB0D46QFTM891",
|
||||
"type": "phone",
|
||||
"number": "XXX-XXX-0100",
|
||||
"name": "",
|
||||
"capabilities": [
|
||||
"push",
|
||||
"sms",
|
||||
"phone"
|
||||
]
|
||||
},
|
||||
{
|
||||
"device": "DHEKH0JJIYC1LX3AZWO4",
|
||||
"type": "token",
|
||||
"name": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
duo := buildAuthApi(ts.URL, nil)
|
||||
|
||||
res, err := duo.Preauth(PreauthUserId("10"), PreauthIpAddr("127.0.0.1"), PreauthTrustedToken("l33t"))
|
||||
if err != nil {
|
||||
t.Error("Failed TestPreauthUserId: " + err.Error())
|
||||
}
|
||||
if res.Stat != "OK" {
|
||||
t.Error("Unexpected stat: " + res.Stat)
|
||||
}
|
||||
if res.Response.Result != "auth" {
|
||||
t.Error("Unexpected response result: " + res.Response.Result)
|
||||
}
|
||||
if res.Response.Status_Msg != "Account is active" {
|
||||
t.Error("Unexpected status message: " + res.Response.Status_Msg)
|
||||
}
|
||||
if len(res.Response.Devices) != 2 {
|
||||
t.Errorf("Unexpected devices length: %d", len(res.Response.Devices))
|
||||
}
|
||||
if res.Response.Devices[0].Device != "DPFZRS9FB0D46QFTM891" {
|
||||
t.Error("Unexpected [0] device name: " + res.Response.Devices[0].Device)
|
||||
}
|
||||
if res.Response.Devices[0].Type != "phone" {
|
||||
t.Error("Unexpected [0] device type: " + res.Response.Devices[0].Type)
|
||||
}
|
||||
if res.Response.Devices[0].Number != "XXX-XXX-0100" {
|
||||
t.Error("Unexpected [0] device number: " + res.Response.Devices[0].Number)
|
||||
}
|
||||
if res.Response.Devices[0].Name != "" {
|
||||
t.Error("Unexpected [0] devices name :" + res.Response.Devices[0].Name)
|
||||
}
|
||||
if len(res.Response.Devices[0].Capabilities) != 3 {
|
||||
t.Errorf("Unexpected [0] device capabilities length: %d", len(res.Response.Devices[0].Capabilities))
|
||||
}
|
||||
if res.Response.Devices[0].Capabilities[0] != "push" {
|
||||
t.Error("Unexpected [0] device capability: " + res.Response.Devices[0].Capabilities[0])
|
||||
}
|
||||
if res.Response.Devices[0].Capabilities[1] != "sms" {
|
||||
t.Error("Unexpected [0] device capability: " + res.Response.Devices[0].Capabilities[1])
|
||||
}
|
||||
if res.Response.Devices[0].Capabilities[2] != "phone" {
|
||||
t.Error("Unexpected [0] device capability: " + res.Response.Devices[0].Capabilities[2])
|
||||
}
|
||||
if res.Response.Devices[1].Device != "DHEKH0JJIYC1LX3AZWO4" {
|
||||
t.Error("Unexpected [1] device name: " + res.Response.Devices[1].Device)
|
||||
}
|
||||
if res.Response.Devices[1].Type != "token" {
|
||||
t.Error("Unexpected [1] device type: " + res.Response.Devices[1].Type)
|
||||
}
|
||||
if res.Response.Devices[1].Name != "0" {
|
||||
t.Error("Unexpected [1] devices name :" + res.Response.Devices[1].Name)
|
||||
}
|
||||
if len(res.Response.Devices[1].Capabilities) != 0 {
|
||||
t.Errorf("Unexpected [1] device capabilities length: %d", len(res.Response.Devices[1].Capabilities))
|
||||
}
|
||||
}
|
||||
|
||||
// Test preauth enroll with username, and an enroll response.
|
||||
func TestPreauthEnroll(t *testing.T) {
|
||||
ts := httptest.NewTLSServer(
|
||||
http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
req_params, err := getBodyParams(r)
|
||||
if err != nil {
|
||||
t.Error("Failed to retrieve body parameters")
|
||||
}
|
||||
if req_params.Get("username") != "10" {
|
||||
t.Error("TestEnrollStatus failed to set 'username' query parameter: " +
|
||||
r.RequestURI)
|
||||
}
|
||||
fmt.Fprintln(w, `
|
||||
{
|
||||
"stat": "OK",
|
||||
"response": {
|
||||
"enroll_portal_url": "https://api-3945ef22.duosecurity.com/portal?48bac5d9393fb2c2",
|
||||
"result": "enroll",
|
||||
"status_msg": "Enroll an authentication device to proceed"
|
||||
}
|
||||
}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
duo := buildAuthApi(ts.URL, nil)
|
||||
|
||||
res, err := duo.Preauth(PreauthUsername("10"))
|
||||
if err != nil {
|
||||
t.Error("Failed TestPreauthEnroll: " + err.Error())
|
||||
}
|
||||
if res.Stat != "OK" {
|
||||
t.Error("Unexpected stat: " + res.Stat)
|
||||
}
|
||||
if res.Response.Enroll_Portal_Url != "https://api-3945ef22.duosecurity.com/portal?48bac5d9393fb2c2" {
|
||||
t.Error("Unexpected enroll portal URL: " + res.Response.Enroll_Portal_Url)
|
||||
}
|
||||
if res.Response.Result != "enroll" {
|
||||
t.Error("Unexpected response result: " + res.Response.Result)
|
||||
}
|
||||
if res.Response.Status_Msg != "Enroll an authentication device to proceed" {
|
||||
t.Error("Unexpected status msg: " + res.Response.Status_Msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Test an authentication request / response. This won't work against the Duo
|
||||
// server, because the request parameters included are illegal. But we can
|
||||
// verify that the go code sets the query parameters correctly.
|
||||
func TestAuth(t *testing.T) {
|
||||
ts := httptest.NewTLSServer(
|
||||
http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
req_params, err := getBodyParams(r)
|
||||
if err != nil {
|
||||
t.Error("Failed to retrieve body parameters")
|
||||
}
|
||||
expected := map[string]string{
|
||||
"username": "username value",
|
||||
"user_id": "user_id value",
|
||||
"factor": "auto",
|
||||
"ipaddr": "40.40.40.10",
|
||||
"async": "1",
|
||||
"device": "primary",
|
||||
"type": "request",
|
||||
"display_username": "display username",
|
||||
}
|
||||
for key, value := range expected {
|
||||
if req_params.Get(key) != value {
|
||||
t.Errorf("TestAuth failed to set '%s' query parameter: "+
|
||||
r.RequestURI, key)
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(w, `
|
||||
{
|
||||
"stat": "OK",
|
||||
"response": {
|
||||
"result": "allow",
|
||||
"status": "allow",
|
||||
"status_msg": "Success. Logging you in..."
|
||||
}
|
||||
}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
duo := buildAuthApi(ts.URL, nil)
|
||||
|
||||
res, err := duo.Auth("auto",
|
||||
AuthUserId("user_id value"),
|
||||
AuthUsername("username value"),
|
||||
AuthIpAddr("40.40.40.10"),
|
||||
AuthAsync(),
|
||||
AuthDevice("primary"),
|
||||
AuthType("request"),
|
||||
AuthDisplayUsername("display username"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Error("Failed TestAuth: " + err.Error())
|
||||
}
|
||||
if res.Stat != "OK" {
|
||||
t.Error("Unexpected stat: " + res.Stat)
|
||||
}
|
||||
if res.Response.Result != "allow" {
|
||||
t.Error("Unexpected response result: " + res.Response.Result)
|
||||
}
|
||||
if res.Response.Status != "allow" {
|
||||
t.Error("Unexpected response status: " + res.Response.Status)
|
||||
}
|
||||
if res.Response.Status_Msg != "Success. Logging you in..." {
|
||||
t.Error("Unexpected response status msg: " + res.Response.Status_Msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Test AuthStatus request / response.
|
||||
func TestAuthStatus(t *testing.T) {
|
||||
ts := httptest.NewTLSServer(
|
||||
http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
expected := map[string]string{
|
||||
"txid": "4",
|
||||
}
|
||||
for key, value := range expected {
|
||||
if r.FormValue(key) != value {
|
||||
t.Errorf("TestAuthStatus failed to set '%s' query parameter: "+
|
||||
r.RequestURI, key)
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(w, `
|
||||
{
|
||||
"stat": "OK",
|
||||
"response": {
|
||||
"result": "waiting",
|
||||
"status": "pushed",
|
||||
"status_msg": "Pushed a login request to your phone..."
|
||||
}
|
||||
}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
duo := buildAuthApi(ts.URL, nil)
|
||||
|
||||
res, err := duo.AuthStatus("4")
|
||||
if err != nil {
|
||||
t.Error("Failed TestAuthStatus: " + err.Error())
|
||||
}
|
||||
|
||||
if res.Stat != "OK" {
|
||||
t.Error("Unexpected stat: " + res.Stat)
|
||||
}
|
||||
if res.Response.Result != "waiting" {
|
||||
t.Error("Unexpected response result: " + res.Response.Result)
|
||||
}
|
||||
if res.Response.Status != "pushed" {
|
||||
t.Error("Unexpected response status: " + res.Response.Status)
|
||||
}
|
||||
if res.Response.Status_Msg != "Pushed a login request to your phone..." {
|
||||
t.Error("Unexpected response status msg: " + res.Response.Status_Msg)
|
||||
}
|
||||
}
|
156
vendor/github.com/duosecurity/duo_api_golang/duo_test.go
generated
vendored
Normal file
156
vendor/github.com/duosecurity/duo_api_golang/duo_test.go
generated
vendored
Normal file
|
@ -0,0 +1,156 @@
|
|||
package duoapi
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCanonicalize(t *testing.T) {
|
||||
values := url.Values{}
|
||||
values.Set("username", "H ell?o")
|
||||
values.Set("password", "H-._~i")
|
||||
values.Add("password", "A(!'*)")
|
||||
params_str := canonicalize("post",
|
||||
"API-XXX.duosecurity.COM",
|
||||
"/auth/v2/ping",
|
||||
values,
|
||||
"5")
|
||||
params := strings.Split(params_str, "\n")
|
||||
if len(params) != 5 {
|
||||
t.Error("Expected 5 parameters, but got " + string(len(params)))
|
||||
}
|
||||
if params[1] != string("POST") {
|
||||
t.Error("Expected POST, but got " + params[1])
|
||||
}
|
||||
if params[2] != string("api-xxx.duosecurity.com") {
|
||||
t.Error("Expected api-xxx.duosecurity.com, but got " + params[2])
|
||||
}
|
||||
if params[3] != string("/auth/v2/ping") {
|
||||
t.Error("Expected /auth/v2/ping, but got " + params[3])
|
||||
}
|
||||
if params[4] != string("password=A%28%21%27%2A%29&password=H-._~i&username=H%20ell%3Fo") {
|
||||
t.Error("Expected sorted escaped params, but got " + params[4])
|
||||
}
|
||||
}
|
||||
|
||||
func encodeAndValidate(t *testing.T, input url.Values, output string) {
|
||||
values := url.Values{}
|
||||
for key, val := range input {
|
||||
values.Set(key, val[0])
|
||||
}
|
||||
params_str := canonicalize("post",
|
||||
"API-XXX.duosecurity.com",
|
||||
"/auth/v2/ping",
|
||||
values,
|
||||
"5")
|
||||
params := strings.Split(params_str, "\n")
|
||||
if params[4] != output {
|
||||
t.Error("Mismatch\n" + output + "\n" + params[4])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestSimple(t *testing.T) {
|
||||
values := url.Values{}
|
||||
values.Set("realname", "First Last")
|
||||
values.Set("username", "root")
|
||||
|
||||
encodeAndValidate(t, values, "realname=First%20Last&username=root")
|
||||
}
|
||||
|
||||
func TestZero(t *testing.T) {
|
||||
values := url.Values{}
|
||||
encodeAndValidate(t, values, "")
|
||||
}
|
||||
|
||||
func TestOne(t *testing.T) {
|
||||
values := url.Values{}
|
||||
values.Set("realname", "First Last")
|
||||
encodeAndValidate(t, values, "realname=First%20Last")
|
||||
}
|
||||
|
||||
func TestPrintableAsciiCharaceters(t *testing.T) {
|
||||
values := url.Values{}
|
||||
values.Set("digits", "0123456789")
|
||||
values.Set("letters", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
values.Set("punctuation", "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~")
|
||||
values.Set("whitespace", "\t\n\x0b\x0c\r ")
|
||||
encodeAndValidate(t, values, "digits=0123456789&letters=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ&punctuation=%21%22%23%24%25%26%27%28%29%2A%2B%2C-.%2F%3A%3B%3C%3D%3E%3F%40%5B%5C%5D%5E_%60%7B%7C%7D~&whitespace=%09%0A%0B%0C%0D%20")
|
||||
}
|
||||
|
||||
func TestSortOrderWithCommonPrefix(t *testing.T) {
|
||||
values := url.Values{}
|
||||
values.Set("foo", "1")
|
||||
values.Set("foo_bar", "2")
|
||||
encodeAndValidate(t, values, "foo=1&foo_bar=2")
|
||||
}
|
||||
|
||||
func TestUnicodeFuzzValues(t *testing.T) {
|
||||
values := url.Values{}
|
||||
values.Set("bar", "⠕ꪣ㟏䮷㛩찅暎腢슽ꇱ")
|
||||
values.Set("baz", "ෳ蒽噩馅뢤갺篧潩鍊뤜")
|
||||
values.Set("foo", "퓎훖礸僀訠輕ﴋ耤岳왕")
|
||||
values.Set("qux", "讗졆-芎茚쳊ꋔ谾뢲馾")
|
||||
encodeAndValidate(t, values, "bar=%E2%A0%95%EA%AA%A3%E3%9F%8F%E4%AE%B7%E3%9B%A9%EC%B0%85%E6%9A%8E%E8%85%A2%EC%8A%BD%EA%87%B1&baz=%E0%B7%B3%E8%92%BD%E5%99%A9%E9%A6%85%EB%A2%A4%EA%B0%BA%E7%AF%A7%E6%BD%A9%E9%8D%8A%EB%A4%9C&foo=%ED%93%8E%ED%9B%96%E7%A4%B8%E5%83%80%E8%A8%A0%E8%BC%95%EF%B4%8B%E8%80%A4%E5%B2%B3%EC%99%95&qux=%E8%AE%97%EC%A1%86-%E8%8A%8E%E8%8C%9A%EC%B3%8A%EA%8B%94%E8%B0%BE%EB%A2%B2%E9%A6%BE")
|
||||
}
|
||||
|
||||
func TestUnicodeFuzzKeysAndValues(t *testing.T) {
|
||||
values := url.Values{}
|
||||
values.Set("䚚⡻㗐軳朧倪ࠐ킑È셰",
|
||||
"ཅ᩶㐚敌숿鬉ꯢ荃ᬧ惐")
|
||||
values.Set("瑉繋쳻姿﹟获귌逌쿑砓",
|
||||
"趷倢鋓䋯⁽蜰곾嘗ॆ丰")
|
||||
values.Set("瑰錔逜麮䃘䈁苘豰ᴱꁂ",
|
||||
"៙ந鍘꫟ꐪ䢾ﮖ濩럿㋳")
|
||||
values.Set("싅Ⱍ☠㘗隳F蘅⃨갡头",
|
||||
"ﮩ䆪붃萋☕㹮攭ꢵ핫U")
|
||||
encodeAndValidate(t, values, "%E4%9A%9A%E2%A1%BB%E3%97%90%E8%BB%B3%E6%9C%A7%E5%80%AA%E0%A0%90%ED%82%91%C3%88%EC%85%B0=%E0%BD%85%E1%A9%B6%E3%90%9A%E6%95%8C%EC%88%BF%E9%AC%89%EA%AF%A2%E8%8D%83%E1%AC%A7%E6%83%90&%E7%91%89%E7%B9%8B%EC%B3%BB%E5%A7%BF%EF%B9%9F%E8%8E%B7%EA%B7%8C%E9%80%8C%EC%BF%91%E7%A0%93=%E8%B6%B7%E5%80%A2%E9%8B%93%E4%8B%AF%E2%81%BD%E8%9C%B0%EA%B3%BE%E5%98%97%E0%A5%86%E4%B8%B0&%E7%91%B0%E9%8C%94%E9%80%9C%E9%BA%AE%E4%83%98%E4%88%81%E8%8B%98%E8%B1%B0%E1%B4%B1%EA%81%82=%E1%9F%99%E0%AE%A8%E9%8D%98%EA%AB%9F%EA%90%AA%E4%A2%BE%EF%AE%96%E6%BF%A9%EB%9F%BF%E3%8B%B3&%EC%8B%85%E2%B0%9D%E2%98%A0%E3%98%97%E9%9A%B3F%E8%98%85%E2%83%A8%EA%B0%A1%E5%A4%B4=%EF%AE%A9%E4%86%AA%EB%B6%83%E8%90%8B%E2%98%95%E3%B9%AE%E6%94%AD%EA%A2%B5%ED%95%ABU")
|
||||
}
|
||||
|
||||
func TestSign(t *testing.T) {
|
||||
values := url.Values{}
|
||||
values.Set("realname", "First Last")
|
||||
values.Set("username", "root")
|
||||
res := sign("DIWJ8X6AEYOR5OMC6TQ1",
|
||||
"Zh5eGmUq9zpfQnyUIu5OL9iWoMMv5ZNmk3zLJ4Ep",
|
||||
"POST",
|
||||
"api-XXXXXXXX.duosecurity.com",
|
||||
"/accounts/v1/account/list",
|
||||
"Tue, 21 Aug 2012 17:29:18 -0000",
|
||||
values)
|
||||
if res != "Basic RElXSjhYNkFFWU9SNU9NQzZUUTE6MmQ5N2Q2MTY2MzE5Nzgx"+
|
||||
"YjVhM2EwN2FmMzlkMzY2ZjQ5MTIzNGVkYw==" {
|
||||
t.Error("Signature did not produce output documented at " +
|
||||
"https://www.duosecurity.com/docs/authapi :(")
|
||||
}
|
||||
}
|
||||
|
||||
func TestV2Canonicalize(t *testing.T) {
|
||||
values := url.Values{}
|
||||
values.Set("䚚⡻㗐軳朧倪ࠐ킑È셰",
|
||||
"ཅ᩶㐚敌숿鬉ꯢ荃ᬧ惐")
|
||||
values.Set("瑉繋쳻姿﹟获귌逌쿑砓",
|
||||
"趷倢鋓䋯⁽蜰곾嘗ॆ丰")
|
||||
values.Set("瑰錔逜麮䃘䈁苘豰ᴱꁂ",
|
||||
"៙ந鍘꫟ꐪ䢾ﮖ濩럿㋳")
|
||||
values.Set("싅Ⱍ☠㘗隳F蘅⃨갡头",
|
||||
"ﮩ䆪붃萋☕㹮攭ꢵ핫U")
|
||||
canon := canonicalize(
|
||||
"PoSt",
|
||||
"foO.BAr52.cOm",
|
||||
"/Foo/BaR2/qux",
|
||||
values,
|
||||
"Fri, 07 Dec 2012 17:18:00 -0000")
|
||||
expected := "Fri, 07 Dec 2012 17:18:00 -0000\nPOST\nfoo.bar52.com\n/Foo/BaR2/qux\n%E4%9A%9A%E2%A1%BB%E3%97%90%E8%BB%B3%E6%9C%A7%E5%80%AA%E0%A0%90%ED%82%91%C3%88%EC%85%B0=%E0%BD%85%E1%A9%B6%E3%90%9A%E6%95%8C%EC%88%BF%E9%AC%89%EA%AF%A2%E8%8D%83%E1%AC%A7%E6%83%90&%E7%91%89%E7%B9%8B%EC%B3%BB%E5%A7%BF%EF%B9%9F%E8%8E%B7%EA%B7%8C%E9%80%8C%EC%BF%91%E7%A0%93=%E8%B6%B7%E5%80%A2%E9%8B%93%E4%8B%AF%E2%81%BD%E8%9C%B0%EA%B3%BE%E5%98%97%E0%A5%86%E4%B8%B0&%E7%91%B0%E9%8C%94%E9%80%9C%E9%BA%AE%E4%83%98%E4%88%81%E8%8B%98%E8%B1%B0%E1%B4%B1%EA%81%82=%E1%9F%99%E0%AE%A8%E9%8D%98%EA%AB%9F%EA%90%AA%E4%A2%BE%EF%AE%96%E6%BF%A9%EB%9F%BF%E3%8B%B3&%EC%8B%85%E2%B0%9D%E2%98%A0%E3%98%97%E9%9A%B3F%E8%98%85%E2%83%A8%EA%B0%A1%E5%A4%B4=%EF%AE%A9%E4%86%AA%EB%B6%83%E8%90%8B%E2%98%95%E3%B9%AE%E6%94%AD%EA%A2%B5%ED%95%ABU"
|
||||
if canon != expected {
|
||||
t.Error("Mismatch!\n" + expected + "\n" + canon)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDuo(t *testing.T) {
|
||||
duo := NewDuoApi("ABC", "123", "api-XXXXXXX.duosecurity.com", "go-client")
|
||||
if duo == nil {
|
||||
t.Fatal("Failed to create a new Duo Api")
|
||||
}
|
||||
}
|
387
vendor/github.com/duosecurity/duo_api_golang/duoapi.go
generated
vendored
Normal file
387
vendor/github.com/duosecurity/duo_api_golang/duoapi.go
generated
vendored
Normal file
|
@ -0,0 +1,387 @@
|
|||
package duoapi
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var spaceReplacer *strings.Replacer = strings.NewReplacer("+", "%20")
|
||||
|
||||
func canonParams(params url.Values) string {
|
||||
// Values must be in sorted order
|
||||
for key, val := range params {
|
||||
sort.Strings(val)
|
||||
params[key] = val
|
||||
}
|
||||
// Encode will place Keys in sorted order
|
||||
ordered_params := params.Encode()
|
||||
// Encoder turns spaces into +, but we need %XX escaping
|
||||
return spaceReplacer.Replace(ordered_params)
|
||||
}
|
||||
|
||||
func canonicalize(method string,
|
||||
host string,
|
||||
uri string,
|
||||
params url.Values,
|
||||
date string) string {
|
||||
var canon [5]string
|
||||
canon[0] = date
|
||||
canon[1] = strings.ToUpper(method)
|
||||
canon[2] = strings.ToLower(host)
|
||||
canon[3] = uri
|
||||
canon[4] = canonParams(params)
|
||||
return strings.Join(canon[:], "\n")
|
||||
}
|
||||
|
||||
func sign(ikey string,
|
||||
skey string,
|
||||
method string,
|
||||
host string,
|
||||
uri string,
|
||||
date string,
|
||||
params url.Values) string {
|
||||
canon := canonicalize(method, host, uri, params, date)
|
||||
mac := hmac.New(sha1.New, []byte(skey))
|
||||
mac.Write([]byte(canon))
|
||||
sig := hex.EncodeToString(mac.Sum(nil))
|
||||
auth := ikey + ":" + sig
|
||||
return "Basic " + base64.StdEncoding.EncodeToString([]byte(auth))
|
||||
}
|
||||
|
||||
type DuoApi struct {
|
||||
ikey string
|
||||
skey string
|
||||
host string
|
||||
userAgent string
|
||||
apiClient *http.Client
|
||||
authClient *http.Client
|
||||
}
|
||||
|
||||
type apiOptions struct {
|
||||
timeout time.Duration
|
||||
insecure bool
|
||||
proxy func(*http.Request) (*url.URL, error)
|
||||
}
|
||||
|
||||
// Optional parameter for NewDuoApi, used to configure timeouts on API calls.
|
||||
func SetTimeout(timeout time.Duration) func(*apiOptions) {
|
||||
return func(opts *apiOptions) {
|
||||
opts.timeout = timeout
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Optional parameter for testing only. Bypasses all TLS certificate validation.
|
||||
func SetInsecure() func(*apiOptions) {
|
||||
return func(opts *apiOptions) {
|
||||
opts.insecure = true
|
||||
}
|
||||
}
|
||||
|
||||
// Optional parameter for NewDuoApi, used to configure an HTTP Connect proxy
|
||||
// server for all outbound communications.
|
||||
func SetProxy(proxy func(*http.Request) (*url.URL, error)) func(*apiOptions) {
|
||||
return func(opts *apiOptions) {
|
||||
opts.proxy = proxy
|
||||
}
|
||||
}
|
||||
|
||||
// Build an return a DuoApi struct.
|
||||
// ikey is your Duo integration key
|
||||
// skey is your Duo integration secret key
|
||||
// host is your Duo host
|
||||
// userAgent allows you to specify the user agent string used when making
|
||||
// the web request to Duo.
|
||||
// options are optional parameters. Use SetTimeout() to specify a timeout value
|
||||
// for Rest API calls. Use SetProxy() to specify proxy settings for Duo API calls.
|
||||
//
|
||||
// Example: duoapi.NewDuoApi(ikey,skey,host,userAgent,duoapi.SetTimeout(10*time.Second))
|
||||
func NewDuoApi(ikey string,
|
||||
skey string,
|
||||
host string,
|
||||
userAgent string,
|
||||
options ...func(*apiOptions)) *DuoApi {
|
||||
opts := apiOptions{proxy: http.ProxyFromEnvironment}
|
||||
for _, o := range options {
|
||||
o(&opts)
|
||||
}
|
||||
|
||||
// Certificate pinning
|
||||
certPool := x509.NewCertPool()
|
||||
certPool.AppendCertsFromPEM([]byte(duoPinnedCert))
|
||||
|
||||
tr := &http.Transport{
|
||||
Proxy: opts.proxy,
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: certPool,
|
||||
InsecureSkipVerify: opts.insecure,
|
||||
},
|
||||
}
|
||||
return &DuoApi{
|
||||
ikey: ikey,
|
||||
skey: skey,
|
||||
host: host,
|
||||
userAgent: userAgent,
|
||||
apiClient: &http.Client{
|
||||
Timeout: opts.timeout,
|
||||
Transport: tr,
|
||||
},
|
||||
authClient: &http.Client{
|
||||
Transport: tr,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type requestOptions struct {
|
||||
timeout bool
|
||||
}
|
||||
|
||||
type DuoApiOption func(*requestOptions)
|
||||
|
||||
// Pass to Request or SignedRequest to configure a timeout on the request
|
||||
func UseTimeout(opts *requestOptions) {
|
||||
opts.timeout = true
|
||||
}
|
||||
|
||||
func (duoapi *DuoApi) buildOptions(options ...DuoApiOption) *requestOptions {
|
||||
opts := &requestOptions{}
|
||||
for _, o := range options {
|
||||
o(opts)
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
// API calls will return a StatResult object. On success, Stat is 'OK'.
|
||||
// On error, Stat is 'FAIL', and Code, Message, and Message_Detail
|
||||
// contain error information.
|
||||
type StatResult struct {
|
||||
Stat string
|
||||
Code *int32
|
||||
Message *string
|
||||
Message_Detail *string
|
||||
}
|
||||
|
||||
// Make an unsigned Duo Rest API call. See Duo's online documentation
|
||||
// for the available REST API's.
|
||||
// method is POST or GET
|
||||
// uri is the URI of the Duo Rest call
|
||||
// params HTTP query parameters to include in the call.
|
||||
// options Optional parameters. Use UseTimeout to toggle whether the
|
||||
// Duo Rest API call should timeout or not.
|
||||
//
|
||||
// Example: duo.Call("GET", "/auth/v2/ping", nil, duoapi.UseTimeout)
|
||||
func (duoapi *DuoApi) Call(method string,
|
||||
uri string,
|
||||
params url.Values,
|
||||
options ...DuoApiOption) (*http.Response, []byte, error) {
|
||||
opts := duoapi.buildOptions(options...)
|
||||
|
||||
client := duoapi.authClient
|
||||
if opts.timeout {
|
||||
client = duoapi.apiClient
|
||||
}
|
||||
|
||||
url := url.URL{
|
||||
Scheme: "https",
|
||||
Host: duoapi.host,
|
||||
Path: uri,
|
||||
RawQuery: params.Encode(),
|
||||
}
|
||||
request, err := http.NewRequest(method, url.String(), nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
resp, err := client.Do(request)
|
||||
var body []byte
|
||||
if err == nil {
|
||||
body, err = ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
}
|
||||
return resp, body, err
|
||||
}
|
||||
|
||||
// Make a signed Duo Rest API call. See Duo's online documentation
|
||||
// for the available REST API's.
|
||||
// method is POST or GET
|
||||
// uri is the URI of the Duo Rest call
|
||||
// params HTTP query parameters to include in the call.
|
||||
// options Optional parameters. Use UseTimeout to toggle whether the
|
||||
// Duo Rest API call should timeout or not.
|
||||
//
|
||||
// Example: duo.SignedCall("GET", "/auth/v2/check", nil, duoapi.UseTimeout)
|
||||
func (duoapi *DuoApi) SignedCall(method string,
|
||||
uri string,
|
||||
params url.Values,
|
||||
options ...DuoApiOption) (*http.Response, []byte, error) {
|
||||
opts := duoapi.buildOptions(options...)
|
||||
|
||||
now := time.Now().UTC().Format(time.RFC1123Z)
|
||||
auth_sig := sign(duoapi.ikey, duoapi.skey, method, duoapi.host, uri, now, params)
|
||||
|
||||
url := url.URL{
|
||||
Scheme: "https",
|
||||
Host: duoapi.host,
|
||||
Path: uri,
|
||||
}
|
||||
method = strings.ToUpper(method)
|
||||
|
||||
if method == "GET" {
|
||||
url.RawQuery = params.Encode()
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(method, url.String(), nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
request.Header.Set("Authorization", auth_sig)
|
||||
request.Header.Set("Date", now)
|
||||
|
||||
if method == "POST" || method == "PUT" {
|
||||
request.Body = ioutil.NopCloser(strings.NewReader(params.Encode()))
|
||||
request.Header.Set("Content-type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
|
||||
client := duoapi.authClient
|
||||
if opts.timeout {
|
||||
client = duoapi.apiClient
|
||||
}
|
||||
resp, err := client.Do(request)
|
||||
var body []byte
|
||||
if err == nil {
|
||||
body, err = ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
}
|
||||
return resp, body, err
|
||||
}
|
||||
|
||||
const duoPinnedCert string = `
|
||||
subject= /C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert Assured ID Root CA
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl
|
||||
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
|
||||
d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv
|
||||
b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG
|
||||
EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl
|
||||
cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi
|
||||
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c
|
||||
JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP
|
||||
mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+
|
||||
wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4
|
||||
VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/
|
||||
AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB
|
||||
AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW
|
||||
BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun
|
||||
pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC
|
||||
dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf
|
||||
fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm
|
||||
NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx
|
||||
H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe
|
||||
+o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g==
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
subject= /C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert Global Root CA
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh
|
||||
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
|
||||
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD
|
||||
QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT
|
||||
MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
|
||||
b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG
|
||||
9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB
|
||||
CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97
|
||||
nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt
|
||||
43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P
|
||||
T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4
|
||||
gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO
|
||||
BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR
|
||||
TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw
|
||||
DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr
|
||||
hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg
|
||||
06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF
|
||||
PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls
|
||||
YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk
|
||||
CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
subject= /C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert High Assurance EV Root CA
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs
|
||||
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
|
||||
d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j
|
||||
ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL
|
||||
MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
|
||||
LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug
|
||||
RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm
|
||||
+9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW
|
||||
PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM
|
||||
xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB
|
||||
Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3
|
||||
hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg
|
||||
EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF
|
||||
MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA
|
||||
FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec
|
||||
nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z
|
||||
eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF
|
||||
hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2
|
||||
Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe
|
||||
vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep
|
||||
+OkuE6N36B9K
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
subject= /C=US/O=SecureTrust Corporation/CN=SecureTrust CA
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI
|
||||
MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x
|
||||
FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz
|
||||
MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv
|
||||
cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN
|
||||
AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz
|
||||
Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO
|
||||
0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao
|
||||
wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj
|
||||
7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS
|
||||
8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT
|
||||
BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB
|
||||
/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg
|
||||
JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC
|
||||
NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3
|
||||
6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/
|
||||
3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm
|
||||
D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS
|
||||
CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR
|
||||
3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
subject= /C=US/O=SecureTrust Corporation/CN=Secure Global CA
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK
|
||||
MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x
|
||||
GTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx
|
||||
MjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg
|
||||
Q29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG
|
||||
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ
|
||||
iQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa
|
||||
/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ
|
||||
jnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI
|
||||
HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7
|
||||
sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w
|
||||
gZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF
|
||||
MAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw
|
||||
KaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG
|
||||
AQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L
|
||||
URYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO
|
||||
H0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm
|
||||
I50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY
|
||||
iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc
|
||||
f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW
|
||||
-----END CERTIFICATE-----`
|
1
vendor/github.com/duosecurity/duo_api_golang/go.mod
generated
vendored
Normal file
1
vendor/github.com/duosecurity/duo_api_golang/go.mod
generated
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
module github.com/duosecurity/duo_api_golang
|
Loading…
Reference in a new issue