mfa: add duo_api_golang dependency to Godeps
This commit is contained in:
parent
112f98d86f
commit
36d60623b1
|
@ -58,6 +58,10 @@
|
|||
"Comment": "v2.0.0-18-gc904d70",
|
||||
"Rev": "c904d7032a70da6551c43929f199244f6a45f4c1"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/duosecurity/duo_api_golang",
|
||||
"Rev": "16da9e74793f6d9b97b227a0696fe32bcdaecb42"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/fatih/structs",
|
||||
"Rev": "a9f7daa9c2729e97450c2da2feda19130a367d8f"
|
||||
|
|
25
Godeps/_workspace/src/github.com/duosecurity/duo_api_golang/LICENSE
generated
vendored
Normal file
25
Godeps/_workspace/src/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.
|
14
Godeps/_workspace/src/github.com/duosecurity/duo_api_golang/README.md
generated
vendored
Normal file
14
Godeps/_workspace/src/github.com/duosecurity/duo_api_golang/README.md
generated
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
# Overview
|
||||
|
||||
**duo_client** - Demonstration client to call Duo API methods
|
||||
with Go.
|
||||
|
||||
# Duo Auth API
|
||||
|
||||
The Duo Auth API provides a low-level API for adding strong two-factor
|
||||
authentication to applications that cannot directly display rich web
|
||||
content.
|
||||
|
||||
For more information see the Duo Auth API guide:
|
||||
|
||||
<http://www.duosecurity.com/docs/authapi>
|
380
Godeps/_workspace/src/github.com/duosecurity/duo_api_golang/authapi/authapi.go
generated
vendored
Normal file
380
Godeps/_workspace/src/github.com/duosecurity/duo_api_golang/authapi/authapi.go
generated
vendored
Normal file
|
@ -0,0 +1,380 @@
|
|||
package authapi
|
||||
|
||||
import(
|
||||
"github.com/duosecurity/duo_api_golang"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type AuthApi struct {
|
||||
api 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: api}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Return object for the 'Ping' API call.
|
||||
type PingResult struct {
|
||||
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.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 {
|
||||
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.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 {
|
||||
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.api.SignedCall("GET", "/auth/v2/logo", nil, duoapi.UseTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode == 200 {
|
||||
ret := &LogoResult{StatResult: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 {
|
||||
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.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 {
|
||||
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.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 {
|
||||
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.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 {
|
||||
StatResult
|
||||
Response struct {
|
||||
// Synchronous
|
||||
Result string
|
||||
Status string
|
||||
Status_Msg 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.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 {
|
||||
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.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
|
||||
}
|
497
Godeps/_workspace/src/github.com/duosecurity/duo_api_golang/authapi/authapi_test.go
generated
vendored
Normal file
497
Godeps/_workspace/src/github.com/duosecurity/duo_api_golang/authapi/authapi_test.go
generated
vendored
Normal file
|
@ -0,0 +1,497 @@
|
|||
package authapi
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"fmt"
|
||||
"strings"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"time"
|
||||
"github.com/duosecurity/duo_api_golang"
|
||||
)
|
||||
|
||||
func buildAuthApi(url string) *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()))
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
start := time.Now()
|
||||
_, err := duo.Ping()
|
||||
duration := time.Since(start)
|
||||
if duration.Seconds() > 2 {
|
||||
t.Error("Timeout took %d 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)
|
||||
|
||||
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)
|
||||
|
||||
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 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)
|
||||
|
||||
_, 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)
|
||||
|
||||
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) {
|
||||
if r.FormValue("username") != "49c6c3097adb386048c84354d82ea63d" {
|
||||
t.Error("TestEnroll failed to set 'username' query parameter:" +
|
||||
r.RequestURI)
|
||||
}
|
||||
if r.FormValue("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)
|
||||
|
||||
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) {
|
||||
if r.FormValue("user_id") != "49c6c3097adb386048c84354d82ea63d" {
|
||||
t.Error("TestEnrollStatus failed to set 'user_id' query parameter:" +
|
||||
r.RequestURI)
|
||||
}
|
||||
if r.FormValue("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)
|
||||
|
||||
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) {
|
||||
if r.FormValue("ipaddr") != "127.0.0.1" {
|
||||
t.Error("TestPreauth failed to set 'ipaddr' query parameter:" +
|
||||
r.RequestURI)
|
||||
}
|
||||
if r.FormValue("user_id") != "10" {
|
||||
t.Error("TestEnrollStatus failed to set 'user_id' query parameter: " +
|
||||
r.RequestURI)
|
||||
}
|
||||
if r.FormValue("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)
|
||||
|
||||
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) {
|
||||
if r.FormValue("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)
|
||||
|
||||
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) {
|
||||
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 r.FormValue(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)
|
||||
|
||||
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)
|
||||
|
||||
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
Godeps/_workspace/src/github.com/duosecurity/duo_api_golang/duo_test.go
generated
vendored
Normal file
156
Godeps/_workspace/src/github.com/duosecurity/duo_api_golang/duo_test.go
generated
vendored
Normal file
|
@ -0,0 +1,156 @@
|
|||
package duoapi
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
355
Godeps/_workspace/src/github.com/duosecurity/duo_api_golang/duoapi.go
generated
vendored
Normal file
355
Godeps/_workspace/src/github.com/duosecurity/duo_api_golang/duoapi.go
generated
vendored
Normal file
|
@ -0,0 +1,355 @@
|
|||
package duoapi
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"net/url"
|
||||
"sort"
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"time"
|
||||
"io/ioutil"
|
||||
)
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// 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{}
|
||||
for _, o := range options {
|
||||
o(&opts)
|
||||
}
|
||||
|
||||
// Certificate pinning
|
||||
certPool := x509.NewCertPool()
|
||||
certPool.AppendCertsFromPEM([]byte(duoPinnedCert))
|
||||
|
||||
tr := &http.Transport{
|
||||
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
|
||||
}
|
||||
|
||||
// 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,
|
||||
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)
|
||||
|
||||
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\n" +
|
||||
"-----BEGIN CERTIFICATE-----\n" +
|
||||
"MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl\n" +
|
||||
"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n" +
|
||||
"d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv\n" +
|
||||
"b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG\n" +
|
||||
"EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl\n" +
|
||||
"cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi\n" +
|
||||
"MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c\n" +
|
||||
"JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP\n" +
|
||||
"mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+\n" +
|
||||
"wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4\n" +
|
||||
"VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/\n" +
|
||||
"AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB\n" +
|
||||
"AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW\n" +
|
||||
"BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun\n" +
|
||||
"pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC\n" +
|
||||
"dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf\n" +
|
||||
"fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm\n" +
|
||||
"NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx\n" +
|
||||
"H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe\n" +
|
||||
"+o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g==\n" +
|
||||
"-----END CERTIFICATE-----\n" +
|
||||
"\n" +
|
||||
"subject= /C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert Global Root CA\n" +
|
||||
"-----BEGIN CERTIFICATE-----\n" +
|
||||
"MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh\n" +
|
||||
"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n" +
|
||||
"d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD\n" +
|
||||
"QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT\n" +
|
||||
"MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\n" +
|
||||
"b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG\n" +
|
||||
"9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB\n" +
|
||||
"CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97\n" +
|
||||
"nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt\n" +
|
||||
"43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P\n" +
|
||||
"T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4\n" +
|
||||
"gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO\n" +
|
||||
"BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR\n" +
|
||||
"TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw\n" +
|
||||
"DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr\n" +
|
||||
"hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg\n" +
|
||||
"06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF\n" +
|
||||
"PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls\n" +
|
||||
"YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk\n" +
|
||||
"CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=\n" +
|
||||
"-----END CERTIFICATE-----\n" +
|
||||
"\n" +
|
||||
"subject= /C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert High Assurance EV Root CA\n" +
|
||||
"-----BEGIN CERTIFICATE-----\n" +
|
||||
"MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs\n" +
|
||||
"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n" +
|
||||
"d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j\n" +
|
||||
"ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL\n" +
|
||||
"MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3\n" +
|
||||
"LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug\n" +
|
||||
"RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm\n" +
|
||||
"+9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW\n" +
|
||||
"PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM\n" +
|
||||
"xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB\n" +
|
||||
"Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3\n" +
|
||||
"hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg\n" +
|
||||
"EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF\n" +
|
||||
"MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA\n" +
|
||||
"FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec\n" +
|
||||
"nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z\n" +
|
||||
"eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF\n" +
|
||||
"hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2\n" +
|
||||
"Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe\n" +
|
||||
"vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep\n" +
|
||||
"+OkuE6N36B9K\n" +
|
||||
"-----END CERTIFICATE-----\n" +
|
||||
"\n" +
|
||||
"subject= /C=US/O=SecureTrust Corporation/CN=SecureTrust CA\n" +
|
||||
"-----BEGIN CERTIFICATE-----\n" +
|
||||
"MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI\n" +
|
||||
"MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x\n" +
|
||||
"FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz\n" +
|
||||
"MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv\n" +
|
||||
"cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN\n" +
|
||||
"AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz\n" +
|
||||
"Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO\n" +
|
||||
"0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao\n" +
|
||||
"wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj\n" +
|
||||
"7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS\n" +
|
||||
"8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT\n" +
|
||||
"BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB\n" +
|
||||
"/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg\n" +
|
||||
"JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC\n" +
|
||||
"NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3\n" +
|
||||
"6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/\n" +
|
||||
"3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm\n" +
|
||||
"D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS\n" +
|
||||
"CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR\n" +
|
||||
"3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE=\n" +
|
||||
"-----END CERTIFICATE-----\n" +
|
||||
"\n" +
|
||||
"subject= /C=US/O=SecureTrust Corporation/CN=Secure Global CA\n" +
|
||||
"-----BEGIN CERTIFICATE-----\n" +
|
||||
"MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK\n" +
|
||||
"MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x\n" +
|
||||
"GTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx\n" +
|
||||
"MjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg\n" +
|
||||
"Q29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG\n" +
|
||||
"SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ\n" +
|
||||
"iQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa\n" +
|
||||
"/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ\n" +
|
||||
"jnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI\n" +
|
||||
"HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7\n" +
|
||||
"sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w\n" +
|
||||
"gZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF\n" +
|
||||
"MAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw\n" +
|
||||
"KaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG\n" +
|
||||
"AQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L\n" +
|
||||
"URYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO\n" +
|
||||
"H0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm\n" +
|
||||
"I50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY\n" +
|
||||
"iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc\n" +
|
||||
"f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW\n" +
|
||||
"-----END CERTIFICATE-----\n"
|
Loading…
Reference in New Issue