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

Implemented TOTP Secret Backend #2492

Merged
merged 48 commits into from
May 4, 2017
Merged

Conversation

mymercurialsky
Copy link
Contributor

@mymercurialsky mymercurialsky commented Mar 15, 2017

Implementation and test file for TOTP secret backend. This resolves #1197.

…t conversion of period when generating passwords
Copy link
Contributor

@briankassouf briankassouf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor things overall. Looking really good!

case "SHA512":
algorithm = otplib.AlgorithmSHA512
default:
algorithm = otplib.AlgorithmSHA1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the default case is ever hit, it seems to me like something went wrong somewhere. Maybe it should return an error here instead.

case 6:
digits = otplib.DigitsSix
case 8:
digits = otplib.DigitsEight
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add a default case that returns an error

"Invalid key value: %s", err)), nil
}

if period < 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

period <= 0

// Set optional parameters if neccessary
if period == 0 {
period = 30
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be removed now that there is a default and if the above branch is changed to <=

entry, err := logical.StorageEntryJSON("role/"+name, &roleEntry{
Key: key,
Issuer: issuer,
Account_Name: account_name,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on Go's best practices Account_Name should be: AccountName

type roleEntry struct {
Key string `json:"key" mapstructure:"key" structs:"key"`
Issuer string `json:"issuer" mapstructure:"issuer" structs:"issuer"`
Account_Name string `json:"account_name" mapstructure:"account_name" structs:"account_name"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Account_Name -> AccountName


resp, err := &logical.Response{
Data: map[string]interface{}{
"token": totpToken,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think the response should be key'ed as "code" rather than "token"?

@briankassouf briankassouf added this to the 0.7.1 milestone Mar 20, 2017
@briankassouf briankassouf requested a review from jefferai March 21, 2017 17:15

//Read digits
digitsQuery, err := strconv.Atoi(urlQuery.Get("digits"))
if err == nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be if err != nil and error out if digits can't be parsed. Same with the period above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Digits, Algorithm and Period are all optional values of a url. The error would come from strconv attempting to parse an empty string. Should I first do a check if the query is empty and if it is, skip that field and if it isn't parse it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep!

case 8:
keyDigits = otplib.DigitsEight
default:
return logical.ErrorResponse("the digit value can only be 6 or 8"), nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/digit/digits

return logical.ErrorResponse("the skew value must be 0 or 1"), nil
}

if qrSize <= 0 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this should be required; if they want to only get a URL back (and the actual key separately) and skip the QR code we shouldn't force it. I'd have qrSize being 0 just disable QR code generation.

//Parse url
urlObject, err := url.Parse(inputURL)
if err != nil {
return logical.ErrorResponse("an error occured while parsing url string"), nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...and what is that error? :-) You may not want to put it in the URL, but then at least log it (probably at debug or info level).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean log the error or log the URL? The URL might contain secret data that we don't want in the logs

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did mean log the error, although I guess depending on what that error is, if it spits out parts of the url it could log something secret. It may be worth returning the error to the caller though, presumably since they provided the URL they can be privy to parts of it in the error message.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless they're automated systems that then log the error automatically. So pick: turtles all the way down, or a potentially but not necessarily useful error message.

SecretSize: uintKeySize,
})
if err != nil {
return logical.ErrorResponse("an error occured while generating a key"), nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, log the error or return it (in this case since the error wouldn't have the key value in it it's probably safe to just return) -- or both!

urlString := keyObject.String()
barcode, err := keyObject.Image(qrSize, qrSize)
if err != nil {
return logical.ErrorResponse("an error occured while generating a QR code image"), nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment...

uintSkew := uint(skew)
uintKeySize := uint(keySize)

var response logical.Response
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this a pointer -- the default value will be nil. That way at the bottom you don't need an if block to switch on generate -- you just return response and if generate was false it'll automatically be nil.

Copy link
Member

@jefferai jefferai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking much better! Some relatively minor comments.


- `name` `(string: <required>)` – Specifies the name of the key to create. This is specified as part of the URL.

- `generate` `(bool: false)` – Specifies if a key is generated by Vault or if a key is being passed from another service.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say "...if a key should be generated by Vault..."


- `key` `(string: <required - if generate is false and url is empty>)` – Specifies the master key used to generate a TOTP code. Only used if generate is false.

- `issuer` `(string: "" <required - if generate is true and url is empty>)` – Specifies the name of the key’s issuing organization.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment here about generate being true and url is empty not matching the actual code and the description of the parameters in the backend.


- `digits` `(int: 6)` – Specifies the number of digits in the generated TOTP code. This value can be set to 6 or 8.

- `skew` `(int: 0)` – Specifies the number of delay periods that are allowed when validating a TOTP code. This value can be either 0 or 1. Only used if generate is true.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A reminder if you default to 1 to change this.

https://vault.rocks/v1/totp/keys/my-key
```

### Sample Response
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add some documentation here indicating that the returned QR code, if any, is a base64-encoded PNG.

}
```

### Embedding Barcodes in HTML
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just saw this; I think you can add the note above, and then just move this text up there along with it, e.g. "If a QR code is returned, it is base64-formatted PNG bytes. You can embed it in a web page by including..."

func pathKeys(b *backend) *framework.Path {
return &framework.Path{
Pattern: "keys/" + framework.GenericNameRegex("name"),
Fields: map[string]*framework.FieldSchema{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a boolean field (exported probably) that defaults to true and that controls whether or not a key that is generated is actually returned as a URL/QR code. The use-case is if you want to give one Vault user access to fetch a code, another access to validate the codes, but you don't ever actually want the key to be exposed.

A neat enhancement in this case might be to have the algorithm default to SHA256 if this is false, because if you know the key is never used anywhere else, you don't have to worry about compatibility issues.

… up error reporting, changed default skew value, updated documentation and added additional tests
@mymercurialsky
Copy link
Contributor Author

I made another round of changes based on Jeff's comments.

return logical.ErrorResponse(fmt.Sprintf("unknown key: %s", name)), nil
}

valid, err := totplib.ValidateCustom(code, key.Key, time.Now(), totplib.ValidateOpts{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're not checking this error -- if there is an error here it should bail out.

}

// Return the secret
resp, err := &logical.Response{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/resp, err :=/return/ ? There's really no reason to define both of these only to return them, just return them directly.

Copy link
Member

@jefferai jefferai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚢 🕺 👯‍♂️ 👯 💃

Oh, sorry -- after those two comments are fixed that is!

… validating codes and clarified documentation for generate parameters in path_keys
@mymercurialsky
Copy link
Contributor Author

I fixed the two issues you mentioned. One note, one of the errors that the validation code generates is if the code is of invalid length (doesn't match digits). I set it to ignore that particular error but I can change it if you want. My thought was that if a code is invalid the backend should just return the false value in the return data.

@jefferai
Copy link
Member

jefferai commented May 4, 2017

That sounds fine!

Copy link
Contributor

@briankassouf briankassouf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great to me! Fantastic work!

@briankassouf briankassouf merged commit 461d658 into hashicorp:master May 4, 2017
@jefferai
Copy link
Member

jefferai commented May 4, 2017

@mymercurialsky So good...seriously stoked about this. Thanks for all of the hard work and perseverance!

@vsivsi
Copy link

vsivsi commented May 8, 2017

Fantastic, now that this is in place, can we expect that using TOTP as a MFA type for userpass authentication won't be far behind? Like was proposed long ago:

#132
#812

Would be a great addition!

@jefferai
Copy link
Member

@vsivsi No -- that's something else entirely. It is on the roadmap, though.

@vsivsi
Copy link

vsivsi commented May 10, 2017

Right, but this update contains a lot of functionality useful for implementing the MFA auth code, no?. Anyway, glad to hear it is on the roadmap because it is necessary.

@jefferai
Copy link
Member

@vsivsi No, it's entirely separate. Code that can be used for MFA has been in a branch for a long time, but it's been waiting on some other core changes to happen.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add support for using 'vault' as multi tenant TOTP 'token' for API mfa access
5 participants