├── .gitignore ├── README.md ├── LICENSE ├── verifier_test.go └── verifier.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-oauth-pkce-code-verifier 2 | [OAuth PKCE](https://tools.ietf.org/html/rfc7636) code_verifier and code_challenge generator implimented golang. 3 | 4 | # How to use 5 | 6 | ```go 7 | // Create code_verifier 8 | v := CreateCodeVerifier() 9 | code_verifier := v.String() 10 | 11 | // Create code_challenge with plain method 12 | code_challenge := v.CodeChallengeS256() 13 | code_challenge_method := "plain" 14 | 15 | // Create code_challenge with S256 method 16 | code_challenge := v.CodeChallengeS256() 17 | code_challenge_method := "S256" 18 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 nirasan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /verifier_test.go: -------------------------------------------------------------------------------- 1 | package go_oauth_pkce_code_verifier 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | ) 7 | 8 | func TestCreateCodeVerifier(t *testing.T) { 9 | v, e := CreateCodeVerifier() 10 | if e != nil { 11 | t.Error(e) 12 | } 13 | testCodeVerifire(t, v) 14 | } 15 | 16 | func testCodeVerifire(t *testing.T, v *CodeVerifier) { 17 | if len(v.Value) < 43 || len(v.Value) > 128 { 18 | t.Errorf("invalid length: %v", v) 19 | } 20 | if _, e := regexp.Match(`[a-zA-Z\-\_\.\~]+`, []byte(v.Value)); e != nil { 21 | t.Errorf("invalid pattern: %v", v) 22 | } 23 | t.Logf("%v", v) 24 | } 25 | 26 | func TestCreateCodeVerifierWithLength(t *testing.T) { 27 | for i := 1; i <= 128; i++ { 28 | v, e := CreateCodeVerifierWithLength(i) 29 | t.Logf("# length: %d", i) 30 | if i < MinLength || i > MaxLength { 31 | if e == nil { 32 | t.Errorf("invalit result: %v, %v", v, e) 33 | } 34 | } else { 35 | 36 | testCodeVerifire(t, v) 37 | } 38 | } 39 | } 40 | 41 | func TestCreateCodeVerifierFromBytes(t *testing.T) { 42 | v, e := CreateCodeVerifierFromBytes([]byte{116, 24, 223, 180, 151, 153, 224, 37, 79, 250, 96, 125, 216, 173, 43 | 187, 186, 22, 212, 37, 77, 105, 214, 191, 240, 91, 88, 5, 88, 83, 44 | 132, 141, 121}) 45 | if e != nil { 46 | t.Error(e) 47 | } 48 | if v.Value != "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" { 49 | t.Errorf("invalid code_verifier: %v", v) 50 | } 51 | t.Logf("%v", v) 52 | } 53 | 54 | // via https://tools.ietf.org/html/rfc7636#appendix-B 55 | func TestCodeVerifier_CodeChallengeS256(t *testing.T) { 56 | v, _ := CreateCodeVerifier() 57 | v.Value = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" 58 | c := v.CodeChallengeS256() 59 | if c != "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" { 60 | t.Errorf("invalid code_challenge value: %v", c) 61 | } 62 | t.Logf("%v, %v", v, c) 63 | } 64 | -------------------------------------------------------------------------------- /verifier.go: -------------------------------------------------------------------------------- 1 | package go_oauth_pkce_code_verifier 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "fmt" 8 | "io" 9 | "strings" 10 | ) 11 | 12 | const ( 13 | DefaultLength = 32 14 | MinLength = 32 15 | MaxLength = 96 16 | ) 17 | 18 | type CodeVerifier struct { 19 | Value string 20 | } 21 | 22 | func CreateCodeVerifier() (*CodeVerifier, error) { 23 | return CreateCodeVerifierWithLength(DefaultLength) 24 | } 25 | 26 | func CreateCodeVerifierWithLength(length int) (*CodeVerifier, error) { 27 | if length < MinLength || length > MaxLength { 28 | return nil, fmt.Errorf("invalid length: %v", length) 29 | } 30 | buf, err := randomBytes(length) 31 | if err != nil { 32 | return nil, fmt.Errorf("failed to generate random bytes: %v", err) 33 | } 34 | return CreateCodeVerifierFromBytes(buf) 35 | } 36 | 37 | func CreateCodeVerifierFromBytes(b []byte) (*CodeVerifier, error) { 38 | return &CodeVerifier{ 39 | Value: encode(b), 40 | }, nil 41 | } 42 | 43 | func (v *CodeVerifier) String() string { 44 | return v.Value 45 | } 46 | 47 | func (v *CodeVerifier) CodeChallengePlain() string { 48 | return v.Value 49 | } 50 | 51 | func (v *CodeVerifier) CodeChallengeS256() string { 52 | h := sha256.New() 53 | h.Write([]byte(v.Value)) 54 | return encode(h.Sum(nil)) 55 | } 56 | 57 | func encode(msg []byte) string { 58 | encoded := base64.StdEncoding.EncodeToString(msg) 59 | encoded = strings.Replace(encoded, "+", "-", -1) 60 | encoded = strings.Replace(encoded, "/", "_", -1) 61 | encoded = strings.Replace(encoded, "=", "", -1) 62 | return encoded 63 | } 64 | 65 | // https://tools.ietf.org/html/rfc7636#section-4.1) 66 | func randomBytes(length int) ([]byte, error) { 67 | const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" 68 | const csLen = byte(len(charset)) 69 | output := make([]byte, 0, length) 70 | for { 71 | buf := make([]byte, length) 72 | if _, err := io.ReadFull(rand.Reader, buf); err != nil { 73 | return nil, fmt.Errorf("failed to read random bytes: %v", err) 74 | } 75 | for _, b := range buf { 76 | // Avoid bias by using a value range that's a multiple of 62 77 | if b < (csLen * 4) { 78 | output = append(output, charset[b%csLen]) 79 | 80 | if len(output) == length { 81 | return output, nil 82 | } 83 | } 84 | } 85 | } 86 | 87 | } 88 | --------------------------------------------------------------------------------