Repository: 99designs/aws-vault (go)

Issue:
oidc time format issues / aws-sdk2-java
Using the `--server` feature the AWS SDK fails when trying to parse the Expiration format used for oidc, the same date format is used for `aws-vault exec $PROFILE --`

`java.time.format.DateTimeParseException: Text '2020-09-10T18:16:52+02:00' could not be parsed at index 19` (char 19 refers to the `+` symbol).

```sh
$ aws-vault exec $OIDC_PROFILE -- env | grep AWS_SESSION_EXP
AWS_SESSION_EXPIRATION=2020-09-10T18:16:52+02:00
```

when doing so using classic PROFILES (ACCESS_KEY, SECRET_ACCESS_KEY), the correct format is used:
```sh
aws-vault exec $CLASSIC_PROFILE -- env | grep AWS_SESSION_EXP
AWS_SESSION_EXPIRATION=2020-09-10T10:45:32Z
```
In the code `RFC3339` explicitly used, but it seems `ISO-8601` with timezone `UTC`is expected.

https://github.com/99designs/aws-vault/blob/2e8df9e12680c2f124f9aa578da0e16ac121a358/vault/oidctokenkeyring.go#L87

I've never coded in go, but if this seems to be legitimate issue, I'd be glad to try it myself.

- [x] I am using the latest release of AWS Vault
- [ ] I have provided my `.aws/config` (redacted if necessary)
- [x] I have provided the debug output using `aws-vault --debug` (redacted if necessary)

debug output:
<details>
<pre>
2020/09/10 11:47:14 aws-vault v6.0.0
2020/09/10 11:47:14 Loading config file /Users/xxx/.aws/config
2020/09/10 11:47:14 Parsing config file /Users/xxx/.aws/config
2020/09/10 11:47:14 [keyring] Considering backends: [keychain]
2020/09/10 11:47:14 [keyring] Querying keychain for service="aws-vault", keychain="aws-vault.keychain"
2020/09/10 11:47:14 [keyring] Found 1 results
2020/09/10 11:47:14 [keyring] Querying keychain for service="aws-vault", keychain="aws-vault.keychain"
2020/09/10 11:47:14 [keyring] Found 1 results
2020/09/10 11:47:14 [keyring] Querying keychain for service="aws-vault", keychain="aws-vault.keychain"
2020/09/10 11:47:14 [keyring] Found 1 results
2020/09/10 11:47:14 [keyring] Querying keychain for service="aws-vault", account="sso.GetRoleCredentials,foo,bar,-62135596800", keychain="aws-vault.keychain"
2020/09/10 11:47:14 [keyring] No results found
2020/09/10 11:47:14 [keyring] Querying keychain for service="aws-vault", account="oidc:https://d-xxx.awsapps.com/start", keychain="aws-vault.keychain"
2020/09/10 11:47:14 [keyring] No results found
2020/09/10 11:47:15 Created new OIDC client (expires at: 2020-12-09 10:47:15 +0100 CET)
2020/09/10 11:47:15 Created OIDC device code for https://d-xxx.awsapps.com/start (expires in: 600s)
2020/09/10 11:47:15 Opening SSO authorization page in browser
Opening the SSO authorization page in your default browser (use Ctrl-C to abort)
https://device.sso.eu-west-1.amazonaws.com/?user_code=xxx
2020/09/10 11:47:24 Created new OIDC access token for https://d-xxx.awsapps.com/start (expires in: 28800s)
2020/09/10 11:47:24 [keyring] Checking keychain status
2020/09/10 11:47:24 [keyring] Keychain status returned nil, keychain exists
2020/09/10 11:47:24 [keyring] Keychain item trusts keyring
2020/09/10 11:47:24 [keyring] Adding service="aws-vault", label="aws-vault oidc token for https://d-xxx.awsapps.com/start (expires 2020-09-10T19:47:24+02:00)", account="oidc:https://d-xxx.awsapps.com/start", trusted=true to osx keychain "aws-vault.keychain"
2020/09/10 11:47:24 Got credentials ****************K5DR for SSO role ROLE (account: xxx), expires in 7h59m58.160075s
2020/09/10 11:47:24 [keyring] Querying keychain for service="aws-vault", keychain="aws-vault.keychain"
2020/09/10 11:47:24 [keyring] Found 2 results
2020/09/10 11:47:24 [keyring] Querying keychain for service="aws-vault", keychain="aws-vault.keychain"
2020/09/10 11:47:24 [keyring] Found 2 results
2020/09/10 11:47:24 [keyring] Checking keychain status
2020/09/10 11:47:24 [keyring] Keychain status returned nil, keychain exists
2020/09/10 11:47:24 [keyring] Keychain item trusts keyring
2020/09/10 11:47:24 [keyring] Adding service="aws-vault", label="aws-vault session for to (expires 2020-09-10T19:47:23+02:00)", account="sso.GetRoleCredentials,foo,bar,xxx" trusted=true to osx keychain "aws-vault.keychain"
</pre>
</details>


Pull request description:
Add a function to format times compatible with aws sdks
Add a function to format times with ISO 8601 format compatible with all AWS SDKs

Fixes #655 

Patch:
diff --git a/cli/exec.go b/cli/exec.go
index 5f521a6..244938c 100644
--- a/cli/exec.go
+++ b/cli/exec.go
@@ -12,6 +12,7 @@ import (
 	"syscall"
 	"time"
 
+	"github.com/99designs/aws-vault/v6/iso8601"
 	"github.com/99designs/aws-vault/v6/server"
 	"github.com/99designs/aws-vault/v6/vault"
 	"github.com/99designs/keyring"
@@ -229,12 +230,11 @@ func execCredentialHelper(input ExecCommandInput, config *vault.Config, creds *c
 		Version:         1,
 		AccessKeyID:     val.AccessKeyID,
 		SecretAccessKey: val.SecretAccessKey,
+		SessionToken:    val.SessionToken,
 	}
-	if val.SessionToken != "" {
-		credentialData.SessionToken = val.SessionToken
-	}
+
 	if credsExpiresAt, err := creds.ExpiresAt(); err == nil {
-		credentialData.Expiration = credsExpiresAt.Format(time.RFC3339)
+		credentialData.Expiration = credsExpiresAt.UTC().Format(time.RFC3339)
 	}
 
 	json, err := json.Marshal(&credentialData)
@@ -267,7 +267,7 @@ func execEnvironment(input ExecCommandInput, config *vault.Config, creds *creden
 	}
 	if expiration, err := creds.ExpiresAt(); err == nil {
 		log.Println("Setting subprocess env: AWS_SESSION_EXPIRATION")
-		env.Set("AWS_SESSION_EXPIRATION", expiration.Format(time.RFC3339))
+		env.Set("AWS_SESSION_EXPIRATION", iso8601.Format(expiration))
 	}
 
 	if !supportsExecSyscall() {
diff --git a/iso8601/iso8601.go b/iso8601/iso8601.go
new file mode 100644
index 0000000..e25aa15
--- /dev/null
+++ b/iso8601/iso8601.go
@@ -0,0 +1,9 @@
+package iso8601
+
+import "time"
+
+// Format outputs an ISO-8601 datetime string from the given time,
+// in a format compatible with all of the AWS SDKs
+func Format(t time.Time) string {
+	return t.UTC().Format(time.RFC3339)
+}
diff --git a/server/ec2server.go b/server/ec2server.go
index 1a6d45d..317bec2 100644
--- a/server/ec2server.go
+++ b/server/ec2server.go
@@ -8,6 +8,7 @@ import (
 	"net/http"
 	"time"
 
+	"github.com/99designs/aws-vault/v6/iso8601"
 	"github.com/aws/aws-sdk-go/aws/credentials"
 )
 
@@ -109,12 +110,12 @@ func credsHandler(creds *credentials.Credentials) http.HandlerFunc {
 
 		err = json.NewEncoder(w).Encode(map[string]interface{}{
 			"Code":            "Success",
-			"LastUpdated":     time.Now().Format(time.RFC3339),
+			"LastUpdated":     iso8601.Format(time.Now()),
 			"Type":            "AWS-HMAC",
 			"AccessKeyId":     val.AccessKeyID,
 			"SecretAccessKey": val.SecretAccessKey,
 			"Token":           val.SessionToken,
-			"Expiration":      credsExpiresAt.Format(time.RFC3339),
+			"Expiration":      iso8601.Format(credsExpiresAt),
 		})
 		if err != nil {
 			http.Error(w, err.Error(), http.StatusInternalServerError)
diff --git a/server/ecsserver.go b/server/ecsserver.go
index 9ad7526..70019cf 100644
--- a/server/ecsserver.go
+++ b/server/ecsserver.go
@@ -8,8 +8,8 @@ import (
 	"log"
 	"net"
 	"net/http"
-	"time"
 
+	"github.com/99designs/aws-vault/v6/iso8601"
 	"github.com/aws/aws-sdk-go/aws/credentials"
 )
 
@@ -72,7 +72,7 @@ func ecsCredsHandler(creds *credentials.Credentials) http.HandlerFunc {
 			"AccessKeyId":     val.AccessKeyID,
 			"SecretAccessKey": val.SecretAccessKey,
 			"Token":           val.SessionToken,
-			"Expiration":      credsExpiresAt.Format(time.RFC3339),
+			"Expiration":      iso8601.Format(credsExpiresAt),
 		})
 		if err != nil {
 			writeErrorMessage(w, err.Error(), http.StatusInternalServerError)


Test patch:
diff --git a/iso8601/iso8601_test.go b/iso8601/iso8601_test.go
new file mode 100644
index 0000000..fbeba07
--- /dev/null
+++ b/iso8601/iso8601_test.go
@@ -0,0 +1,24 @@
+package iso8601
+
+import (
+	"testing"
+	"time"
+)
+
+func TestFormat(t *testing.T) {
+	input, _ := time.Parse(time.RFC3339, "2009-02-04T21:00:57-08:00")
+	want := "2009-02-05T05:00:57Z"
+	result := Format(input)
+	if result != want {
+		t.Errorf("expected %s for %q got %s", want, input, result)
+	}
+}
+
+func TestFormatForIssue655(t *testing.T) {
+	input, _ := time.Parse(time.RFC3339, "2020-09-10T18:16:52+02:00")
+	want := "2020-09-10T16:16:52Z"
+	result := Format(input)
+	if result != want {
+		t.Errorf("expected %s for %q got %s", want, input, result)
+	}
+}