Skip to content

Commit

Permalink
Merge pull request #54 from shogo82148/support-non-integer-timestamp
Browse files Browse the repository at this point in the history
support non integer timestamp
  • Loading branch information
shogo82148 authored Sep 29, 2021
2 parents 13a49cc + b6cf0f5 commit 46c683c
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 18 deletions.
56 changes: 56 additions & 0 deletions provider/github-app-token/github/oidc/date.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package oidc

import (
"math"
"math/big"
"strconv"
"time"
)

// NumericDate represents a JSON numeric date value, as referenced at
// https://datatracker.ietf.org/doc/html/rfc7519#section-2.
type NumericDate struct {
time.Time
}

func (date NumericDate) MarshalJSON() (b []byte, err error) {
// the maximum time.Time that in Go
const maxTime = "9223371974719179007.999999999"

buf := make([]byte, 0, len(maxTime))
sec := date.Unix()
buf = strconv.AppendInt(buf, sec, 10)

if nsec := date.Nanosecond(); nsec != 0 {
buf = append(buf, '.')
digits := 100_000_000
for nsec != 0 {
d := nsec / digits
buf = append(buf, byte('0'+d))
nsec = nsec % digits
digits /= 10
}
}
return buf, nil
}

var v1_000_000_000 = new(big.Float).SetInt64(1_000_000_000)

func (date *NumericDate) UnmarshalJSON(b []byte) (err error) {
z := new(big.Float).SetPrec(128)
if err := z.UnmarshalText(b); err != nil {
return err
}
sec, acc := z.Int64()
if acc == big.Exact {
// z is an integer, we don't need to parse nsec.
date.Time = time.Unix(sec, 0)
return nil
}

z = z.Sub(z, new(big.Float).SetInt64(sec))
z = z.Mul(z, v1_000_000_000)
nsec, _ := z.Float64()
date.Time = time.Unix(sec, int64(math.RoundToEven(nsec)))
return nil
}
90 changes: 90 additions & 0 deletions provider/github-app-token/github/oidc/date_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package oidc

import (
"encoding/json"
"testing"
"time"
)

func TestNumericDate_MarshalJSON(t *testing.T) {
testCases := []struct {
output string
date time.Time
}{
{
output: "1234567890",
date: time.Unix(1234567890, 0),
},
{
output: "1234567890.123456789",
date: time.Unix(1234567890, 123_456_789),
},
{
output: "1234567890.123456",
date: time.Unix(1234567890, 123_456_000),
},
{
output: "1234567890.1",
date: time.Unix(1234567890, 100_000_000),
},
{
// the maximum time.Time that Go can marshal to JSON.
output: "253402300799.999999999",
date: time.Date(9999, time.December, 31, 23, 59, 59, 999_999_999, time.UTC),
},
{
// the maximum time.Time that in Go
// https://stackoverflow.com/questions/25065055/what-is-the-maximum-time-time-in-go
output: "9223371974719179007.999999999",
date: time.Unix(1<<63-62135596801, 999999999),
},
}

for _, tc := range testCases {
got, err := json.Marshal(NumericDate{tc.date})
if err != nil {
t.Errorf("failed to marshal %s", tc.date)
continue
}
if string(got) != tc.output {
t.Errorf("mashal %s not match: want %s, got %s", tc.date, tc.output, string(got))
}
}
}

func TestNumericDate_UnmarshalJSON(t *testing.T) {
testCases := []struct {
input string
date time.Time
}{
{
input: "1234567890",
date: time.Unix(1234567890, 0),
},
{
input: "1234567890.123456789",
date: time.Unix(1234567890, 123456789),
},
{
// the maximum time.Time that Go can marshal to JSON.
input: "253402300799.999999999",
date: time.Date(9999, time.December, 31, 23, 59, 59, 999_999_999, time.UTC),
},
{
// the maximum time.Time that in Go
// https://stackoverflow.com/questions/25065055/what-is-the-maximum-time-time-in-go
input: "9223371974719179007.999999999",
date: time.Unix(1<<63-62135596801, 999999999),
},
}

for _, tc := range testCases {
var got NumericDate
if err := json.Unmarshal([]byte(tc.input), &got); err != nil {
t.Errorf("failed parse %q: %v", tc.input, err)
}
if !got.Equal(tc.date) {
t.Errorf("the result of %q is unexpected: want %s, got %s", tc.input, tc.date, got)
}
}
}
28 changes: 13 additions & 15 deletions provider/github-app-token/github/parse_id_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@ import (
"errors"
"fmt"
"time"

"github.com/shogo82148/actions-github-app-token/provider/github-app-token/github/oidc"
)

type ActionsIDToken struct {
// common jwt parameters
Audience string `json:"aud,omitempty"`
ExpiresAt int64 `json:"exp,omitempty"`
Id string `json:"jti,omitempty"`
IssuedAt int64 `json:"iat,omitempty"`
Issuer string `json:"iss,omitempty"`
NotBefore int64 `json:"nbf,omitempty"`
Subject string `json:"sub,omitempty"`
Audience string `json:"aud,omitempty"`
ExpiresAt *oidc.NumericDate `json:"exp,omitempty"`
Id string `json:"jti,omitempty"`
IssuedAt *oidc.NumericDate `json:"iat,omitempty"`
Issuer string `json:"iss,omitempty"`
NotBefore *oidc.NumericDate `json:"nbf,omitempty"`
Subject string `json:"sub,omitempty"`

// GitHub's extara parameters
Ref string `json:"ref,omitempty"`
Expand Down Expand Up @@ -51,22 +53,18 @@ func (token *ActionsIDToken) Valid() error {
return fmt.Errorf("github: unexpected issuer: %q", token.Issuer)
}

if token.ExpiresAt == 0 {
if token.ExpiresAt == nil {
return errors.New("github: the exp (expires at) parameter is not set")
}
truncatedTime := now.Truncate(time.Second).Unix()
if truncatedTime >= token.ExpiresAt {
if token.ExpiresAt.Before(now) {
return errors.New("github: the token is already expired")
}

if token.NotBefore == 0 {
if token.NotBefore == nil {
return errors.New("github: the nbf (not before) paremeter is not set")
}

// the not before parameter might be a future time, because GitHub rounds off it.
// we rounds up the current time here to accept such a case.
roundedUpTime := now.Add(time.Second - 1).Truncate(time.Second).Unix()
if roundedUpTime < token.NotBefore {
if now.Before(token.NotBefore.Time) {
return errors.New("github: the token is not valid yet")
}

Expand Down
6 changes: 3 additions & 3 deletions provider/github-app-token/github/parse_id_token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ func TestParseIDToken_Intergrated(t *testing.T) {
t.Logf("sub: %s", id.Subject)
t.Logf("job_workflow_ref: %s", id.JobWorkflowRef)
t.Logf("aud: %s", id.Audience)
t.Logf("issued at %s", time.Unix(id.IssuedAt, 0))
t.Logf("not before %s", time.Unix(id.NotBefore, 0))
t.Logf("expires at %s", time.Unix(id.ExpiresAt, 0))
t.Logf("issued at %s", id.IssuedAt)
t.Logf("not before %s", id.NotBefore)
t.Logf("expires at %s", id.ExpiresAt)

if got, want := id.Actor, os.Getenv("GITHUB_ACTOR"); got != want {
t.Errorf("unexpected actor: want %q, got %q", want, got)
Expand Down

0 comments on commit 46c683c

Please sign in to comment.