Skip to content

Commit

Permalink
Merge branch 'googleauth'
Browse files Browse the repository at this point in the history
加上使用 Google Authenticator 的認證機制

使用 git 應該培養出時常開 branch 的習慣,開發完成再 merge --no-ff 回來,
這樣你可以在 commit log 寫下這整條分支到底做了什麼,日後要查的時候會方便
非常多。

合回來之前最好也把分支名稱改成比較有意義的名字,那你就可以用
git log | grep -A 5 keyword 這種方式來快速找到特定的開發歷程。
  • Loading branch information
Ronmi committed Jul 17, 2016
2 parents c7fca10 + 6271910 commit 56fbcea
Show file tree
Hide file tree
Showing 16 changed files with 505 additions and 91 deletions.
22 changes: 20 additions & 2 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,28 @@ Order 物件由以下四個欄位組成,通通都是必要欄位

# API 列表

## /api/auth

使用者認證,並取回 token

### 參數

`{pin: "6 digit string"}`

`pin` 碼必須是 6 個數字

### 回傳值

成功的話傳回 token 字串,密碼錯誤則是 400 Bad Request

## /api/listall

一次列出所有交易資料

### 參數

`{token: "token string"}`

### 回傳值

`[Order object, Order object, ...]`
Expand All @@ -37,7 +55,7 @@ Order 物件由以下四個欄位組成,通通都是必要欄位

### 參數

`{code: "currency code"}`
`{code: "currency code", token: "token string"}`

`code` 必須是三碼英文,不分大小寫。

Expand All @@ -53,4 +71,4 @@ Order 物件由以下四個欄位組成,通通都是必要欄位

### 參數

`Order object`
`{data: Order object, token: "token string"}`
23 changes: 18 additions & 5 deletions cmd/xchg/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,38 @@ import (

type add struct {
M *sdm.Manager
A Authenticator
}

func (h *add) Handle(enc *json.Encoder, dec *json.Decoder, httpData *jsonapi.HTTP) {
var param Order
type p struct {
Data Order `json:"data"`
Token string `json:"token"`
}

var param p
if err := dec.Decode(&param); err != nil {
httpData.WriteHeader(http.StatusBadRequest)
enc.Encode("Parameter is not Order object")
return
}

if !h.A.Valid(param.Token) {
httpData.WriteHeader(http.StatusForbidden)
enc.Encode("Invalid token")
return
}

// validating data
param.Code = strings.ToUpper(strings.TrimSpace(param.Code))
if len(param.Code) != 3 || param.Local == 0 || param.Foreign == 0 || param.Time <= 0 {
data := param.Data
data.Code = strings.ToUpper(strings.TrimSpace(data.Code))
if len(data.Code) != 3 || data.Local == 0 || data.Foreign == 0 || data.Time <= 0 {
httpData.WriteHeader(http.StatusBadRequest)
enc.Encode("Parameter is not Order object")
enc.Encode("Parameter has no Order object")
return
}

if _, err := h.M.Insert("orders", param); err != nil {
if _, err := h.M.Insert("orders", data); err != nil {
httpData.WriteHeader(http.StatusInternalServerError)
enc.Encode(fmt.Sprintf("Error saving order: %s", err))
return
Expand Down
68 changes: 46 additions & 22 deletions cmd/xchg/add_test.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,35 @@
package main

import (
"log"
"net/http"
"strings"
"testing"

"git.ronmi.tw/ronmi/sdm"

"github.com/Patrolavia/jsonapi"
)

func TestAddOK(t *testing.T) {
mgr, err := initDB([]Order{})
func makeAdd(preset []Order) (*add, string, *sdm.Manager) {
mgr, err := initDB(preset)
if err != nil {
t.Fatalf("Cannot initial database: %s", err)
log.Fatalf("Cannot initial database: %s", err)
}
fake := FakeAuthenticator("123456")
token, _ := fake.Token("123456")
return &add{mgr, fake}, token, mgr
}

func TestAddOK(t *testing.T) {
h, token, mgr := makeAdd([]Order{})
defer mgr.Connection().Close()
h := &add{mgr}

resp, err := jsonapi.HandlerTest(h.Handle).Post("/api/add", "", `{"when":1468248043,"foreign":100,"local":-100,"code":"AUD"}`)
resp, err := jsonapi.HandlerTest(h.Handle).Post(
"/api/add",
"",
`{"data":{"when":1468248043,"foreign":100,"local":-100,"code":"AUD"},"token":"`+token+`"}`,
)

if err != nil {
t.Fatalf("unexpected error occured when testing add: %s", err)
Expand Down Expand Up @@ -61,14 +74,14 @@ func TestAddDuplicated(t *testing.T) {
presetData := []Order{
Order{1468248039, 100, -100, "USD"},
}
mgr, err := initDB(presetData)
if err != nil {
t.Fatalf("Cannot initial database: %s", err)
}
h, token, mgr := makeAdd(presetData)
defer mgr.Connection().Close()
h := &add{mgr}

resp, err := jsonapi.HandlerTest(h.Handle).Post("/api/add", "", `{"when":1468248039,"foreign":100,"local":-100,"code":"USD"}`)
resp, err := jsonapi.HandlerTest(h.Handle).Post(
"/api/add",
"",
`{"data":{"when":1468248039,"foreign":100,"local":-100,"code":"USD"},"token":"`+token+`"}`,
)

if err != nil {
t.Fatalf("unexpected error occured when testing add: %s", err)
Expand All @@ -80,14 +93,10 @@ func TestAddDuplicated(t *testing.T) {
}

func TestAddNoData(t *testing.T) {
mgr, err := initDB([]Order{})
if err != nil {
t.Fatalf("Cannot initial database: %s", err)
}
h, token, mgr := makeAdd([]Order{})
defer mgr.Connection().Close()
h := &add{mgr}

resp, err := jsonapi.HandlerTest(h.Handle).Post("/api/add", "", `{}`)
resp, err := jsonapi.HandlerTest(h.Handle).Post("/api/add", "", `{"token":"`+token+`"}`)

if err != nil {
t.Fatalf("unexpected error occured when testing add: %s", err)
Expand All @@ -99,12 +108,8 @@ func TestAddNoData(t *testing.T) {
}

func TestAddNotJSON(t *testing.T) {
mgr, err := initDB([]Order{})
if err != nil {
t.Fatalf("Cannot initial database: %s", err)
}
h, _, mgr := makeAdd([]Order{})
defer mgr.Connection().Close()
h := &add{mgr}

resp, err := jsonapi.HandlerTest(h.Handle).Post("/api/add", "", `1234`)

Expand All @@ -116,3 +121,22 @@ func TestAddNotJSON(t *testing.T) {
t.Fatalf("unexpected status code %d for bas request: %s", resp.Code, resp.Body.String())
}
}

func TestAddWrongToken(t *testing.T) {
h, _, mgr := makeAdd([]Order{})
defer mgr.Connection().Close()

resp, err := jsonapi.HandlerTest(h.Handle).Post(
"/api/add",
"",
`{"data":{"when":1468248043,"foreign":100,"local":-100,"code":"AUD"},"token":"1234"}`,
)

if err != nil {
t.Fatalf("unexpected error occured when testing add: %s", err)
}

if resp.Code != http.StatusForbidden {
t.Fatalf("expect add return forbidden when wrong token, got %d: %s", resp.Code, resp.Body.String())
}
}
44 changes: 44 additions & 0 deletions cmd/xchg/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package main

import (
"encoding/json"
"net/http"

"github.com/Patrolavia/jsonapi"
)

type auth struct {
A Authenticator
}

// 錯誤處理會重複使用,抽出來
func (h *auth) e(enc *json.Encoder, httpData *jsonapi.HTTP, msg string) {
httpData.WriteHeader(http.StatusBadRequest)
enc.Encode(msg)
return
}

func (h *auth) Handle(enc *json.Encoder, dec *json.Decoder, httpData *jsonapi.HTTP) {
// 定義參數型別
type p struct {
Pin string `json:"pin"`
}

var param p

// 同樣的錯誤會在這個方法裡重複使用,所以拉出來

if err := dec.Decode(&param); err != nil {
h.e(enc, httpData, "Parameter is not Pin object")
return
}

// validating data
token, err := h.A.Token(param.Pin)
if err != nil {
h.e(enc, httpData, "Pin code incorrect")
return
}

enc.Encode(token)
}
53 changes: 53 additions & 0 deletions cmd/xchg/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package main

import (
"net/http"
"testing"

"github.com/Patrolavia/jsonapi"
)

func TestAuthOK(t *testing.T) {
fake := FakeAuthenticator("123456")
h := &auth{fake}

resp, err := jsonapi.HandlerTest(h.Handle).Post("/api/auth", "", `{"pin":"123456"}`)

if err != nil {
t.Fatalf("unexpected error occured when testing auth: %s", err)
}

if resp.Code != http.StatusOK {
t.Fatalf("auth: sent correct pin but got http status %d", resp.Code)
}

if resp.Body == nil {
t.Fatal("auth: got 200 OK, but no token")
}
}

func TestAuthWrongPin(t *testing.T) {
fake := FakeAuthenticator("123456")
h := &auth{fake}

cases := []struct {
in string
msg string
}{
{`{"pin":"654321"}`, "auth: sent wrong pin, expect 400, got %d"},
{`{}`, "auth: sent no pin, expect 400, got %d"},
{`"123456"`, "auth: sent wrong format, expect 400, got %d"},
}

for _, c := range cases {
resp, err := jsonapi.HandlerTest(h.Handle).Post("/api/auth", "", c.in)

if err != nil {
t.Fatalf("unexpected error occured when testing auth: %s", err)
}

if resp.Code != http.StatusBadRequest {
t.Errorf(c.msg, resp.Code)
}
}
}
74 changes: 74 additions & 0 deletions cmd/xchg/authenticator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package main

import (
"math/rand"
"strconv"
"sync"

"github.com/dgryski/dgoogauth"
)

// Authenticator 包裝了 google OTP 和 token 的管理
type Authenticator interface {
Token(pin string) (string, error)
Valid(token string) bool
URI(user string) string // 取得認證用 uri
}

// ErrPincode 代表 pin 碼檢查失敗
type ErrPincode struct {
Pin string
}

func (e ErrPincode) Error() string {
return "pin code " + e.Pin + " incorrect"
}

type authenticator struct {
current string // current token
otp *dgoogauth.OTPConfig
lock *sync.Mutex
}

func (a *authenticator) Token(pin string) (string, error) {
// 避免 data racing 所以上個鎖
a.lock.Lock()
defer a.lock.Unlock()

if ok, err := a.otp.Authenticate(pin); err != nil || !ok {
return "", ErrPincode{pin}
}

// 產生隨機的 token
// 最完美的方法:用亂數然後轉字串(不對
token := a.current
for token == a.current {
token = strconv.Itoa(rand.Int())
}

a.current = token

return token, nil
}

func (a *authenticator) Valid(token string) bool {
return token == a.current
}

func (a *authenticator) URI(user string) string {
return a.otp.ProvisionURIWithIssuer(user, "xchg")
}

// NewAuthenticator 建立一個 10 秒間隔的 totp Authenticator
//
// secret 是 80bit 經過 base32 編碼後的字串
func NewAuthenticator(secret string) Authenticator {
return &authenticator{
"",
&dgoogauth.OTPConfig{
Secret: secret,
WindowSize: 10,
},
&sync.Mutex{},
}
}
Loading

0 comments on commit 56fbcea

Please sign in to comment.