1
0
Fork 0
mirror of https://github.com/Luzifer/nginx-sso.git synced 2024-12-20 21:01: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>

commit 416af9eed8
Author: Ben Edmunds <bensammy2@yahoo.co.uk>
Date:   Fri Dec 28 19:35:26 2018 +0000

    Duo MFA, clean up & documentation

commit 0e511023f0
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:
Knut Ahlers 2018-12-28 23:54:11 +01:00
parent f958f16e51
commit 9e0907f494
Signed by: luzifer
GPG key ID: DC2729FDD34BE99E
13 changed files with 4415 additions and 0 deletions

13
Gopkg.lock generated
View file

@ -37,6 +37,17 @@
revision = "3cfea5ab600ae37946be2b763b8ec2c1cf2d272d" revision = "3cfea5ab600ae37946be2b763b8ec2c1cf2d272d"
version = "v1.0.0" version = "v1.0.0"
[[projects]]
branch = "master"
digest = "1:6911d618517b98a7b59bf3f244ce648a29c81e2c4413f2cda9224a1d2a9b4840"
name = "github.com/duosecurity/duo_api_golang"
packages = [
".",
"authapi",
]
pruneopts = ""
revision = "61e0defebf22863592a27013b399685d0def12b6"
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:b7eb152b61f41f0c9bcb6cf1187e1470a91d5e01d1e08f8b7bfd722578851720" digest = "1:b7eb152b61f41f0c9bcb6cf1187e1470a91d5e01d1e08f8b7bfd722578851720"
@ -183,6 +194,8 @@
"github.com/GeertJohan/yubigo", "github.com/GeertJohan/yubigo",
"github.com/Luzifer/go_helpers/str", "github.com/Luzifer/go_helpers/str",
"github.com/Luzifer/rconfig", "github.com/Luzifer/rconfig",
"github.com/duosecurity/duo_api_golang",
"github.com/duosecurity/duo_api_golang/authapi",
"github.com/flosch/pongo2", "github.com/flosch/pongo2",
"github.com/gorilla/context", "github.com/gorilla/context",
"github.com/gorilla/sessions", "github.com/gorilla/sessions",

View file

@ -188,6 +188,26 @@ attributes:
<mapping of 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 #### 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!) 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!)

View file

@ -43,6 +43,12 @@ mfa:
client_id: "12345" client_id: "12345"
secret_key: "foobar" secret_key: "foobar"
duo:
ikey: "IKEY"
skey: "SKEY"
host: "HOST"
user_agent: "nginx-sso"
providers: providers:
# Authentication against an Atlassian Crowd directory server # Authentication against an Atlassian Crowd directory server
# Supports: Users, Groups # Supports: Users, Groups
@ -96,6 +102,7 @@ providers:
# MFA configs: Username to configs mapping # MFA configs: Username to configs mapping
mfa: mfa:
luzifer: luzifer:
- provider: duo
- provider: google - provider: google
attributes: attributes:
secret: MZXW6YTBOIFA secret: MZXW6YTBOIFA

131
mfa_duo.go Normal file
View 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
View 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
View 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).

View 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(&params)
}
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(&params)
}
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(&params)
}
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(&params)
}
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(&params)
}
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(&params)
}
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(&params)
}
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(&params)
}
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(&params)
}
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
}

File diff suppressed because it is too large Load diff

View 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(&params)
}
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
}

View 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)
}
}

View 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
View 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
View file

@ -0,0 +1 @@
module github.com/duosecurity/duo_api_golang