Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect yubikey #1

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,9 @@ To save the credentials to a custom file, use the `-w` flag.
To print the credentials to the shell instead of storing them in a file, use the `-s` flag. This
will output shell commands which can be pasted in any shell to use the credentials.

To select a specific MFA device by name instead of choosing from a list, use the `-m` flag. The
configuration field `global.mfa-device` may also be set.

### Storing the password in the key chain

> WARNING: Storing the password without having MFA enabled is a security risk. It allows anyone
Expand All @@ -313,6 +316,10 @@ AWS recommends using [regional STS endpoints](https://docs.aws.amazon.com/sdkref

To use a regional endpoint, specify the region via the `global.aws-region` field in the config file. A per app configuration using `apps.<app>.aws-region` is also possible.

## YubiKey Autodetection

YubiKey Autodetection is available for the OneLogin provider. To enable this feature set the `global.autodetect-yubikey` field to `true`. Clisso will look at attached USB devices and automatically select the YubiKey as an MFA device if it is available.

## Caveats and Limitations

- No support for Okta applications with MFA enabled **at the application level**.
Expand Down
10 changes: 10 additions & 0 deletions cmd/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

var printToShell bool
var writeToFile string
var mfaDevice string

func init() {
RootCmd.AddCommand(cmdGet)
Expand All @@ -37,6 +38,15 @@ func init() {
if err != nil {
log.Fatalf("Error binding flag global.credentials-path: %v", err)
}
cmdGet.Flags().StringVarP(
&mfaDevice, "mfa-device", "m", "",
"Specify an MFA device to use (OneLogin Only)",
)
// Bind mfa-device to viper so it can be easily accessed.
err = viper.BindPFlag("global.mfa-device", cmdGet.Flags().Lookup("mfa-device"))
if err != nil {
log.Fatalf("Error binding flag global.mfa-device: %v", err)
}
}

// processCredentials prints the given Credentials to a file and/or to the shell.
Expand Down
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ require (
github.com/crewjam/saml v0.4.14
github.com/go-ini/ini v1.67.0
github.com/icza/gog v0.0.0-20230509085756-00e776132a34
github.com/karalabe/hid v1.0.0
github.com/mattn/go-colorable v0.1.13
github.com/mitchellh/go-homedir v1.1.0
github.com/olekukonko/tablewriter v0.0.5
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.17.0
github.com/stretchr/testify v1.8.4
github.com/zalando/go-keyring v0.2.3
golang.org/x/net v0.19.0
golang.org/x/term v0.15.0
Expand All @@ -37,6 +39,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.1 // indirect
github.com/beevik/etree v1.2.0 // indirect
github.com/danieljoos/wincred v1.2.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fatih/color v1.14.1 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
Expand All @@ -49,6 +52,7 @@ require (
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/russellhaering/goxmldsig v1.4.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
github.com/karalabe/hid v1.0.0 h1:+/CIMNXhSU/zIJgnIvBD2nKHxS/bnRHhhs9xBryLpPo=
github.com/karalabe/hid v1.0.0/go.mod h1:Vr51f8rUOLYrfrWDFlV12GGQgM5AT8sVh+2fY4MPeu8=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
Expand Down
76 changes: 74 additions & 2 deletions onelogin/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,20 @@ import (
"github.com/allcloud-io/clisso/keychain"
"github.com/allcloud-io/clisso/saml"
"github.com/allcloud-io/clisso/spinner"
"github.com/allcloud-io/clisso/yubikey"
"github.com/icza/gog"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
)

const (
// MFADeviceOneLoginProtect symbolizes the OneLogin Protect mobile app, which supports push
// notifications. More info here: https://developers.onelogin.com/api-docs/1/saml-assertions/verify-factor
MFADeviceOneLoginProtect = "OneLogin Protect"

// MFADeviceYubicoYubiKey symbolizes a Yubico YubiKey device.
MFADeviceYubicoYubiKey = "Yubico YubiKey"

// MFAPushTimeout represents the number of seconds to wait for a successful push attempt before
// falling back to OTP input.
MFAPushTimeout = 30
Expand All @@ -38,6 +43,48 @@ var (
keyChain = keychain.DefaultKeychain{}
)

type DeviceOptions struct {
// Detect if a YubiKey is inserted and automatically select the device
AutodetectYubiKey bool

// Override all other choices and select this device name if available
MfaDevice string
}

// NewDeviceOptions returns a configured pointer to a DeviceOptions type
func NewDeviceOptions() *DeviceOptions {
d := new(DeviceOptions)
d.setAutodetectYubiKey()
d.setMfaDevice()

log.WithFields(log.Fields{
"autodetectYubiKey": d.AutodetectYubiKey,
"MfaDevice": d.MfaDevice,
}).Debug("created device options configuration")

return d
}

// setAutodetectYubiKey sets the AutodetectYubiKey parameter
func (d *DeviceOptions) setAutodetectYubiKey() {
var a bool
if viper.IsSet("global.autodetect-yubikey") {
a = viper.GetBool("global.autodetect-yubikey")
}
if a && yubikey.IsAttached() {
d.AutodetectYubiKey = true
return
}
}

// setMfaDevice sets the MfaDevice parameter based on user input
func (d *DeviceOptions) setMfaDevice() {
if viper.IsSet("global.mfa-device") {
d.MfaDevice = viper.GetString("global.mfa-device")
return
}
}

// Get gets temporary credentials for the given app.
// TODO Move AWS logic outside this function.
func Get(app, provider, pArn, awsRegion string, duration int32) (*aws.Credentials, error) {
Expand Down Expand Up @@ -122,7 +169,10 @@ func Get(app, provider, pArn, awsRegion string, duration int32) (*aws.Credential

devices := rSaml.Devices
log.WithField("Devices", devices).Trace("Devices returned by GenerateSamlAssertion")
device, err := getDevice(devices)

deviceOpts := NewDeviceOptions()

device, err := getDevice(devices, deviceOpts)
if err != nil {
return nil, fmt.Errorf("error getting devices: %s", err)
}
Expand Down Expand Up @@ -233,7 +283,7 @@ func Get(app, provider, pArn, awsRegion string, duration int32) (*aws.Credential

// getDevice gets a slice of MFA devices, prompts the user to select one and returns the selected device.
// If the slice contains only a single device, that device is returned. If the slice is empty, an error is returned.
func getDevice(devices []Device) (device *Device, err error) {
func getDevice(devices []Device, opts *DeviceOptions) (device *Device, err error) {
if len(devices) == 0 {
// This should never happen
err = errors.New("no MFA device returned by Onelogin")
Expand All @@ -246,6 +296,28 @@ func getDevice(devices []Device) (device *Device, err error) {
return
}

if opts.MfaDevice != "" {
for _, d := range devices {
if d.DeviceType == opts.MfaDevice {
device = &d
fmt.Printf("MFA device %s found, automatically selecting it.\n", opts.MfaDevice)
return
}
}
// If the user requested device is not found, fall through and continue the device selection process.
fmt.Printf("MFA device %s not found.\n", opts.MfaDevice)
}

if opts.AutodetectYubiKey {
for _, d := range devices {
if d.DeviceType == MFADeviceYubicoYubiKey {
device = &d
fmt.Println("YubiKey detected, automatically selecting it.")
return
}
}
}

var selection int
for {
for i, d := range devices {
Expand Down
77 changes: 77 additions & 0 deletions onelogin/get_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package onelogin

import (
"errors"
"testing"

"github.com/stretchr/testify/assert"
)

func TestGetDevice(t *testing.T) {

var deviceList = []Device{
{
DeviceID: 01,
DeviceType: "Yubico YubiKey",
},
{
DeviceID: 02,
DeviceType: "OneLogin Protect",
},
{
DeviceID: 03,
DeviceType: "Google Authenticator",
},
}

cases := []struct {
Name string
Devices []Device
Opts *DeviceOptions
ExpectedDevice *Device
ExpectedError error
}{
{
Name: "NoDevices",
Devices: []Device{},
Opts: &DeviceOptions{},
ExpectedDevice: nil,
ExpectedError: errors.New("no MFA device returned by Onelogin"),
},
{
Name: "AutodetectYubiKey",
Devices: deviceList,
Opts: &DeviceOptions{AutodetectYubiKey: true},
ExpectedDevice: &Device{DeviceID: 01, DeviceType: "Yubico YubiKey"},
ExpectedError: nil,
},
{
Name: "SelectedMfaDevice",
Devices: deviceList,
Opts: &DeviceOptions{MfaDevice: "Google Authenticator"},
ExpectedDevice: &Device{DeviceID: 03, DeviceType: "Google Authenticator"},
ExpectedError: nil,
},
{
Name: "SelectedMfaDeviceOverride",
Devices: deviceList,
Opts: &DeviceOptions{AutodetectYubiKey: true, MfaDevice: "Google Authenticator"},
ExpectedDevice: &Device{DeviceID: 03, DeviceType: "Google Authenticator"},
ExpectedError: nil,
},
}

for _, c := range cases {
t.Run(c.Name, func(t *testing.T) {

d, err := getDevice(c.Devices, c.Opts)
assert.Equal(t, c.ExpectedDevice, d)
assert.Equal(t, c.ExpectedError, err)
})
}
}
1 change: 1 addition & 0 deletions sample_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ apps:
role-arn: arn:aws:iam::123456789012:role/OktaDevSSO
url: https://xxxxxxxx.oktapreview.com/home/amazon_aws/xxxxxxxxxxxxxxxxxxxx/137
global:
autodetect-yubikey: true
aws-region: us-east-1
credentials-path: ~/.aws/credentials
selected-app: sample-app-1
Expand Down
31 changes: 31 additions & 0 deletions yubikey/yubikey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package yubikey

import (
"github.com/karalabe/hid"
log "github.com/sirupsen/logrus"
)

// IsAttached queries the connected USB devices and returns true if a YubiKey is attached
func IsAttached() bool {
var yubiKeyVendorID uint16 = 0x1050

// List all USB devices matching the YubiKey vendor ID
devices := hid.Enumerate(yubiKeyVendorID, 0)

if len(devices) == 0 {
log.Debug("No YubiKey device detected")
return false
}

// Log information about the detected YubiKey(s)
if log.GetLevel() == log.DebugLevel {
for _, device := range devices {
log.WithFields(log.Fields{
"vid": device.VendorID,
"pid": device.ProductID,
"product": device.Product,
}).Debug("YubiKey device detected")
}
}
return true
}
Loading