Upgrade to Pro — share decks privately, control downloads, hide ads and more …

OAuth 2.0 サーバをGo言語で実装しテストを書く at golang.tokyo#18

OAuth 2.0 サーバをGo言語で実装しテストを書く at golang.tokyo#18

golang.tokyo #18 で発表した内容です。#golang #golangtokyo

https://golangtokyo.connpass.com/event/101174/

Kazuki Higashiguchi

September 28, 2018
Tweet

More Decks by Kazuki Higashiguchi

Other Decks in Technology

Transcript

  1. About this talk background • ಛఆΫϥΠΞϯτʹର͢ΔೝূɾೝՄ • ͱ͋ΔAPIαʔόʹOAuth 2.0ͰͷೝՄΛ ड͚࣋ͭRequest

    HandlerΛ࣮૷͢Δ • ೝՄαʔόͱϦιʔεαʔόͷಉډ • OAuth2.0ܥϥΠϒϥϦΛ༻͍࣮ͯ૷͠ɺ ςετΛॻ͘
  2. ࣗݾ঺հ • ౦ޱ ࿨ᏻ @Khigashiguchi • Server Side EngineerʢGo /

    PHPʣ • BASE, Inc / BASE Product Division • Blog: http:// khigashigashi.hatenablog.com/
  3. About OAuth 2.0 • The OAuth 2.0 Authorization Framework •

    ݖݶͷೝՄʢAuthorizationʣΛߦ͏ͨΊ ͷΦʔϓϯελϯμʔυ • RFC6749 • https://tools.ietf.org/html/rfc6749 • RFC6750 • https://tools.ietf.org/html/rfc6750
  4. OAuth 2.0 Authorization Grant Type • 4ͭͷೝՄλΠϓ͕RFC6749#4ʹͯఆٛ͞ Ε͍ͯΔ • Authorization

    Code Grant (#4.1) • Implicit Grant (#4.2) • Resource Owner Password Credentials Grant (#4.3) • Client Credentials Grant (#4.4)
  5. Which type I choose?: background again • ΫϥΠΞϯτ - αʔόؒͷೝূɾೝՄ

    • ϢʔβʔͷೝՄΛඞཁͱ͠ͳ͍ • ෆಛఆଟ਺Ͱ͸ͳ͘ɺಛఆΫϥΠΞϯτʹ ର͢ΔೝূɾೝՄ
  6. Grant Type: Client Credentials • “The client can request an

    access token using only its client credentials” https://qiita.com/awakia/items/66975de18ba25f18a961 https://tools.ietf.org/html/rfc6749#section-4.4
  7. OAuth 2.0 Authorization Grant Type • Authorization Code Grant (#4.1)

    • Implicit Grant (#4.2) • Resource Owner Password Credentials Grant (#4.3) • Client Credentials Grant (#4.4) Client Credentials GrantͰ࣮૷͍ͯ͘͠
  8. Way to implement OAuth Server • Use SaaS • OAuth

    2.0ܥͷSaaSΛར༻͢Δ • Create Auth Server • OAuth ServerΛผ్࡞੒͢Δ • Implement Monolithic • Request Handlerͱ࣮ͯ͠૷
  9. Way to implement OAuth Server Outline Merit Demerit Use SaaS

    OAuth 2.0ܥ ͷSaaSΛར༻ ɾ։ൃ޻਺୹ॖ ɾϓϩόΠμʔʹ ΑΔηΩϡΞ ɾར༻ྉۚ Create Auth Server OAuth Server Λ࡞੒͢Δ ɾϦιʔεαʔό ͱͷૄ݁߹ ɾશମߏ੒ͷ ෳࡶ౓ Implement Monolithic Request Handlerͱ͠ ࣮ͯ૷ ɾશମߏ੒ͷ ؆ૉԽ ɾີ݁߹
  10. Way to implement OAuth Server Outline Merit Demerit Use SaaS

    OAuth 2.0ܥ ͷSaaSΛར༻ ɾ։ൃ޻਺୹ॖ ɾϓϩόΠμʔʹ ΑΔηΩϡΞ ɾར༻ྉۚ Create Auth Server OAuth Server Λ࡞੒͢Δ ɾϦιʔεαʔό ͱͷૄ݁߹ ɾશମߏ੒ͷ ෳࡶ౓ Implement Monolithic Request Handlerͱ͠ ࣮ͯ૷ ɾશମߏ੒ͷ ؆ૉԽ ɾີ݁߹ APIن໛ΛؑΈͯɺMonolithicʹ࣮૷͢Δ
  11. Packages to implement auth handler • openshift/osin • OSIN is

    an OAuth2 server library for the Go language • https://github.com/openshift/osin • Star: 1477 (2018/9/28) • ory/fosite • Extensible security first OAuth 2.0 and OpenID Connect SDK for Go. • https://github.com/ory/fosite • Star: 1108 (2018/9/28)
  12. Packages to implement auth handler • openshift/osin • OSIN is

    an OAuth2 server library for the Go language • https://github.com/openshift/osin • Star: 1477 (2018/9/28) • ory/fosite • Extensible security first OAuth 2.0 and OpenID Connect SDK for Go. • https://github.com/ory/fosite • Star: 1108 (2018/9/28) ࠓճ͸ͪ͜ΒͰ࣮૷ΛਐΊ·͢
  13. Hello openshift/osin // TokenHandler handle request to get access token.

    func () TokenHandler(w http.ResponseWriter, r *http.Request) { server := osin.NewServer(osin.NewServerConfig(), ex.NewTestStorage()) resp := server.NewResponse() defer resp.Close() if ar := server.HandleAccessRequest(resp, r); ar != nil { ar.Authorized = true server.FinishAccessRequest(resp, r, ar) } if err := osin.OutputJSON(resp, w, r); err != nil { RespondErrorJSON(w, res) return } return } handler.go
  14. Hello openshift/osin // TokenHandler handle request to get access token.

    func () TokenHandler(w http.ResponseWriter, r *http.Request) { server := osin.NewServer(osin.NewServerConfig(), ex.NewTestStorage()) resp := server.NewResponse() defer resp.Close() if ar := server.HandleAccessRequest(resp, r); ar != nil { ar.Authorized = true server.FinishAccessRequest(resp, r, ar) } if err := osin.OutputJSON(resp, w, r); err != nil { RespondErrorJSON(w, res) return } return } handler.go http handlerΛ࣮૷͢Δ
  15. Hello openshift/osin // TokenHandler handle request to get access token.

    func () TokenHandler(w http.ResponseWriter, r *http.Request) { server := osin.NewServer(osin.NewServerConfig(), ex.NewTestStorage()) resp := server.NewResponse() defer resp.Close() if ar := server.HandleAccessRequest(resp, r); ar != nil { ar.Authorized = true server.FinishAccessRequest(resp, r, ar) } if err := osin.OutputJSON(resp, w, r); err != nil { RespondErrorJSON(w, res) return } return } handler.go OAuth server༻ͷinstanceੜ੒ ͦͷࡍɺConfigurationɾStorageΛ౉͢
  16. Hello openshift/osin // TokenHandler handle request to get access token.

    func () TokenHandler(w http.ResponseWriter, r *http.Request) { server := osin.NewServer(osin.NewServerConfig(), ex.NewTestStorage()) resp := server.NewResponse() defer resp.Close() if ar := server.HandleAccessRequest(resp, r); ar != nil { ar.Authorized = true server.FinishAccessRequest(resp, r, ar) } if err := osin.OutputJSON(resp, w, r); err != nil { RespondErrorJSON(w, res) return } return } handler.go RequestΛॲཧ
  17. Hello openshift/osin // TokenHandler handle request to get access token.

    func () TokenHandler(w http.ResponseWriter, r *http.Request) { server := osin.NewServer(osin.NewServerConfig(), ex.NewTestStorage()) resp := server.NewResponse() defer resp.Close() if ar := server.HandleAccessRequest(resp, r); ar != nil { ar.Authorized = true server.FinishAccessRequest(resp, r, ar) } if err := osin.OutputJSON(resp, w, r); err != nil { RespondErrorJSON(w, res) return } return } handler.go ResponseΛॻ͖ࠐΉ
  18. Hello openshift/osin > % curl -X POST -u 1234:abcd \

    http://localhost:8080/oauth2/token?grant_type=client_credentials {“access_token":"4eFkLh4IRzG_73lklQlM-A","expires_in": 3600,"token_type":"Bearer"} Console
  19. Configuration &ServerConfig{ AuthorizationExpiration: 250, AccessExpiration: 3600, TokenType: "Bearer", AllowedAuthorizeTypes: AllowedAuthorizeType{CODE},

    AllowedAccessTypes: AllowedAccessType{AUTHORIZATION_CODE}, ErrorStatusCode: 200, AllowClientSecretInParams: false, AllowGetAccessRequest: false, RetainTokenAfterRefresh: false, } osin/config.go • OAuth Serverͱͯ͠ͷઃఆΛ͢ΔɻඞཁʹԠ ͯ͡ઃఆΛมߋ͢Δɻ
  20. Storage // Storage interface type Storage interface { Clone() Storage

    Close() GetClient(id string) (Client, error) SaveAuthorize(*AuthorizeData) error LoadAuthorize(code string) (*AuthorizeData, error) RemoveAuthorize(code string) error SaveAccess(*AccessData) error LoadAccess(token string) (*AccessData, error) RemoveAccess(token string) error LoadRefresh(token string) (*AccessData, error) RemoveRefresh(token string) error } osin/storage.go • osin.Storage interfaceΛຬͨ͢Α͏ʹɺσʔλͷ CRUDͳͲΛߦ͏StorageΛ࣮૷͢Δɻ
  21. Storage // Storage interface type Storage interface { Clone() Storage

    Close() GetClient(id string) (Client, error) SaveAuthorize(*AuthorizeData) error LoadAuthorize(code string) (*AuthorizeData, error) RemoveAuthorize(code string) error SaveAccess(*AccessData) error LoadAccess(token string) (*AccessData, error) RemoveAccess(token string) error LoadRefresh(token string) (*AccessData, error) RemoveRefresh(token string) error } osin/storage.go • ೝՄλΠϓʹΑͬͯෆඞཁͳ΋ͷ΋ग़ͯ͘Δɺ ͦͷ৔߹͸ۭ࣮૷ɻ Client Credentials Ͱ͸ɺ ԫ৭࿮ͷΈͰे෼
  22. Logger // TokenHandler handle request to get access token. func

    () TokenHandler(w http.ResponseWriter, r *http.Request) { server := osin.NewServer(osin.NewServerConfig(), ex.NewTestStorage()) resp := server.NewResponse() defer resp.Close() if ar := server.HandleAccessRequest(resp, r); ar != nil { ar.Authorized = true server.FinishAccessRequest(resp, r, ar) } if err := osin.OutputJSON(resp, w, r); err != nil { RespondErrorJSON(w, res) return } return } handler.go osin.NewServer()Ͱฦͬͯ͘Δɺ ߏ଄ମͷϑΟʔϧυʹΞΫηε͢Δ͜ͱͰɺ loggerΛࠩ͠ସ͑Δ͜ͱ͕Ͱ͖Δɻ
  23. Logger // Server is an OAuth2 implementation type Server struct

    { Config *ServerConfig Storage Storage AuthorizeTokenGen AuthorizeTokenGen AccessTokenGen AccessTokenGen Now func() time.Time Logger Logger } osin/server.go • osin.NewServer()Ͱ͸ҎԼͷ*osin.Server struct͕ฦΓ஋
  24. Logger // Server is an OAuth2 implementation type Server struct

    { Config *ServerConfig Storage Storage AuthorizeTokenGen AuthorizeTokenGen AccessTokenGen AccessTokenGen Now func() time.Time Logger Logger } osin/server.go • Logger fieldΛࠩ͠ସ͑Δɺosin.Logger interface Λຬ࣮ͨ͢૷Λ͢Δ
  25. Logger type Logger interface { Printf(format string, v ...interface{}) }

    osin/log.go • ࠩ͠ସ͑ΔLogger interface͸ɺPrintf()Λ࣋ͭ ࣮૷Λ͢Δ͚ͩɻ
  26. Logger // Writer specifies output of logger. var Writer io.Writer

    = os.Stdout type OauthLogger struct { } func (*OauthLogger) Printf(format string, v ...interface{}) { fmt.Fprintf(Writer, fmt.Sprintf(“OauthServerError: "+format, v...)) } logger.go • Logger interfaceΛຬ࣮ͨͨ͠૷
  27. Logger Testing func TestServerLogger_Printf(t *testing.T) { r, w, err :=

    os.Pipe() if err != nil { t.Errorf("unexpected by os.Pipe(): %#v", err) } logger.Writer = w l := logger.OauthLogger{} l.Printf("%s %s %s", "test", "test1", "test2") w.Close() actual, err := ioutil.ReadAll(r) if err != nil { t.Errorf("unexpected by ioutil.ReadAll(): %#v", err) } expected := "OauthServerError: test test1 test2" if diff := cmp.Diff(expected, string(actual)); diff != "" { t.Errorf("differs: (-want +got)\n%s", diff) } } logger_test.go
  28. Logger Testing func TestServerLogger_Printf(t *testing.T) { r, w, err :=

    os.Pipe() if err != nil { t.Errorf("unexpected by os.Pipe(): %#v", err) } logger.Writer = w l := logger.OauthLogger{} l.Printf("%s %s %s", "test", "test1", "test2") w.Close() actual, err := ioutil.ReadAll(r) if err != nil { t.Errorf("unexpected by ioutil.ReadAll(): %#v", err) } expected := "OauthServerError: test test1 test2" if diff := cmp.Diff(expected, string(actual)); diff != "" { t.Errorf("differs: (-want +got)\n%s", diff) } } logger_test.go os.Pipe()Ͱ༻ҙͨ͠*FileΛɺ logger.Writerʹ୅ೖ͢Δ͜ͱͰɺ logग़ྗΛςετ͢Δ
  29. Refactoring Handler type Oauth2Controller struct { OauthServer *osin.Server } //

    NewOauth2Controller returns Oauth2.0 handler instance. func NewOauth2Controller(db repository.DB) Oauth2Controller { // Oauth server instanceੜ੒ͷͨΊʹඞཁͳ΋ͷΛ४උ storage := storage.NewOauthServerStorage(db) config := config.NewOauthServerConfig() logging := logger.OauthLogger{} server := osin.NewServer(config, storage) server.Logger = &logging return Oauth2Controller{ OauthServer: server, } } handler.go
  30. Refactoring Handler type Oauth2Controller struct { OauthServer *osin.Server } //

    NewOauth2Controller returns Oauth2.0 handler instance. func NewOauth2Controller(db repository.DB) Oauth2Controller { // Oauth server instanceੜ੒ͷͨΊʹඞཁͳ΋ͷΛ४උ storage := storage.NewOauthServerStorage(db) config := config.NewOauthServerConfig() logging := logger.OauthLogger{} server := osin.NewServer(config, storage) server.Logger = &logging return Oauth2Controller{ OauthServer: server, } } handler.go *osin.Server Λfieldʹ࣋ͭɺControllerߏ଄ମΛ࡞Δ
  31. Refactoring Handler type Oauth2Controller struct { OauthServer *osin.Server } func

    NewOauth2Controller(db repository.DB) Oauth2Controller { config := config.NewOauthServerConfig() storage := storage.NewOauthServerStorage(db) logging := logger.OauthLogger{} server := osin.NewServer(config, storage) server.Logger = &logging return Oauth2Controller{ OauthServer: server, } } handler.go Config, Storage, LoggerΛೖΕͨ*osin.ServerΛ࡞Δ
  32. Refactoring Handler type Oauth2Controller struct { OauthServer *osin.Server } //

    TokenHandler handle request to get access token. func (c *Oauth2Controller) TokenHandler(w http.ResponseWriter, r *http.Request) { resp := c.OauthServer.NewResponse() defer resp.Close() if ar := c.OauthServer.HandleAccessRequest(resp, r); ar != nil { ar.Authorized = true c.OauthServer.FinishAccessRequest(resp, r, ar) } if err := osin.OutputJSON(resp, w, r); err != nil { RespondErrorJSON(w, res) return } return } handler.go
  33. Refactoring Handler type Oauth2Controller struct { OauthServer *osin.Server } //

    TokenHandler handle request to get access token. func (c *Oauth2Controller) TokenHandler(w http.ResponseWriter, r *http.Request) { resp := c.OauthServer.NewResponse() defer resp.Close() if ar := c.OauthServer.HandleAccessRequest(resp, r); ar != nil { ar.Authorized = true c.OauthServer.FinishAccessRequest(resp, r, ar) } if err := osin.OutputJSON(resp, w, r); err != nil { RespondErrorJSON(w, res) return } return } handler.go ߏ଄ମͷϑΟʔϧυʹ࣋ͬͨOauthServerΛར༻ͯ͠ɺ ϥΠϒϥϦͷػೳΛݺͼग़͢ɻ
  34. Complete OAuth Token Handler > % curl -X POST -u

    “client_id”:”client_secret” \ http://localhost:8080/oauth2/token?grant_type=client_credentials {“access_token”:"4eFkLh4IRzG_73lklQlM-A","expires_in": 3600,"token_type":"Bearer"} Console
  35. Testing Handler: mock Storage // Storage interface type Storage interface

    { Clone() Storage Close() GetClient(id string) (Client, error) SaveAuthorize(*AuthorizeData) error LoadAuthorize(code string) (*AuthorizeData, error) RemoveAuthorize(code string) error SaveAccess(*AccessData) error LoadAccess(token string) (*AccessData, error) RemoveAccess(token string) error LoadRefresh(token string) (*AccessData, error) RemoveRefresh(token string) error } osin/storage.go • ෮शɿosin.Storage interface
  36. Testing Handler: mock Storage // mockStorage implements osin.Storage interface. type

    mockStorage struct { getClientResult osin.Client getClientErr error saveAccessErr error } func (m *mockStorage) GetClient(id string) (osin.Client, error) { return m.getClientResult, m.getClientErr } func (m *mockStorage) SaveAccess(*osin.AccessData) error { return m.saveAccessErr } // ͦͷଞɺඞཁͳϝιουΛ࣋ͭ handler_test.go
  37. Testing Handler: mock Storage // mockStorage implements osin.Storage interface. type

    mockStorage struct { getClientResult osin.Client getClientErr error saveAccessErr error } func (m *mockStorage) GetClient(id string) (osin.Client, error) { return m.getClientResult, m.getClientErr } func (m *mockStorage) SaveAccess(*osin.AccessData) error { return m.saveAccessErr } // ͦͷଞɺඞཁͳϝιουΛ࣋ͭ handler_test.go ಈతʹϞοΫͷ໭Γ஋͕ม͑ΕΔΑ͏ʹɺ ςʔϒϧۦಈςετʹͯέʔε͝ͱʹ஋͕ม͑ΒΕΔΑ͏ʹ͢Δɻ
  38. Testing Handler: mock AccessTokenGen // Server is an OAuth2 implementation

    type Server struct { Config *ServerConfig Storage Storage AuthorizeTokenGen AuthorizeTokenGen AccessTokenGen AccessTokenGen Now func() time.Time Logger Logger } osin/server.go • ෮शɿosin.Server struct
  39. Testing Handler: mocking Storage // Server is an OAuth2 implementation

    type Server struct { Config *ServerConfig Storage Storage AuthorizeTokenGen AuthorizeTokenGen AccessTokenGen AccessTokenGen Now func() time.Time Logger Logger } osin/server.go • AccessTokenGen fieldΛࠩ͠ସ͑Δ͜ͱͰϞοΫԽ͢Δ
  40. Testing Handler: mocking Storage // AccessTokenGen generates access tokens type

    AccessTokenGen interface { GenerateAccessToken(data *AccessData, generaterefresh bool) (accesstoken string, refreshtoken string, err error) } osin/access.go • AccessTokenGen interfaceΛຬͨ͢ϞοΫ࣮૷Λߦ͏
  41. Testing Handler // prepare httptest w := httptest.NewRecorder() r =

    httptest.NewRequest("POST", "/oauth2/token?grant_type=client_credentials", nil) r.SetBasicAuth(“client_id", “client_secret”) // create mock oauth server ms := mockStorage{ getClientResult: &osin.DefaultClient{ Id: clientID, Secret: clientSecret, RedirectUri: “http://example.com“, UserData: 1, }, getClientErr: nil, saveAccessErr: nil, } cf := config.NewOauthServerConfig() tg := mockTokenGen{ mockToken: “test-token”, } oServer := osin.NewServer(cf, &ms) oServer.AccessTokenGen = &tg // execute handler c := controller.Oauth2Controller{ OauthServer: oServer, } c.TokenHandler(w, r) res := w.Result() defer res.Body.Close() //ɹҎԼɺΞαʔγϣϯ handler_test.go
  42. Testing Handler // prepare httptest w := httptest.NewRecorder() r =

    httptest.NewRequest("POST", "/oauth2/token?grant_type=client_credentials", nil) r.SetBasicAuth(“client_id", “client_secret”) // create mock oauth server ms := mockStorage{ getClientResult: &osin.DefaultClient{ Id: clientID, Secret: clientSecret, RedirectUri: “http://example.com“, UserData: 1, }, getClientErr: nil, saveAccessErr: nil, } cf := config.NewOauthServerConfig() tg := mockTokenGen{ mockToken: “test-token”, } oServer := osin.NewServer(cf, &ms) oServer.AccessTokenGen = &tg // execute handler c := controller.Oauth2Controller{ OauthServer: oServer, } c.TokenHandler(w, r) res := w.Result() defer res.Body.Close() //ɹҎԼɺΞαʔγϣϯ handler_test.go ࣮ߦલʹϞοΫʹࠩ͠ସ͍͑ͯ͘ɺҎޙͷεϥΠυͰৄࡉɻ
  43. Testing Handler // create mock oauth server ms := mockStorage{

    getClientResult: &osin.DefaultClient{ Id: clientID, Secret: clientSecret, RedirectUri: “http://example.com“, UserData: 1, }, getClientErr: nil, saveAccessErr: nil, } cf := config.NewOauthServerConfig() tg := mockTokenGen{ mockToken: “test-token”, } oServer := osin.NewServer(cf, &ms) oServer.AccessTokenGen = &tg // execute handler c := controller.Oauth2Controller{ OauthServer: oServer, } handler_test.go StorageͷmockΛ࡞੒
  44. Testing Handler // create mock oauth server ms := mockStorage{

    getClientResult: &osin.DefaultClient{ Id: clientID, Secret: clientSecret, RedirectUri: “http://example.com“, UserData: 1, }, getClientErr: nil, saveAccessErr: nil, } cf := config.NewOauthServerConfig() tg := mockTokenGen{ mockToken: “test-token”, } oServer := osin.NewServer(cf, &ms) oServer.AccessTokenGen = &tg // execute handler c := controller.Oauth2Controller{ OauthServer: oServer, } handler_test.go access_tokenੜ੒ΛϞοΫʹࠩ͠ସ͑ɺ”test-token”͕ҰҙʹฦΔ Α͏ʹɻ
  45. Testing Handler // ҎલɺલεϥΠυ಺༰ c.TokenHandler(w, r) res := w.Result() defer

    res.Body.Close() if expected := http.StatusOK; expected != res.StatusCode { t.Errorf("expected status code: %#v, given code: %#v", expected, res.StatusCode) } if expected := "application/json; charset=utf-8"; expected != res.Header.Get("Content- Type") { t.Errorf("unexpected response Content-Type, expected: %#v, but given #%v", expected, res.Header.Get("Content-Type")) } expected := resFormat{ AccessToken: token, ExpiresIn: 3600, TokenType: "Bearer", } actual := resFormat{} if err := json.NewDecoder(res.Body).Decode(&actual); err != nil { t.Errorf("unexpected error occured: %#v, given response: %#v", err, res) } if diff := cmp.Diff(expected, actual); diff != "" { t.Errorf("differs: (-want +got)\n%s", diff) } handler_test.go github.com/google/go-cmp/cmp Ͱͷɺ Ξαʔγϣϯ
  46. Todo list • ✔ ConfigurationʢOAuth serverͷઃఆʣ • ✔ StorageʢDatabase storageʣ

    • ✔ Logger • ✔ Refactoring Handler • ✔ Testing Handler
  47. ·ͱΊ • Client credentials GrantΛ࣮ݱ͢ΔͨΊͷ Access Token EndpointΛ࣮૷ͨ͠ɻ • OAuth

    2.0 ͷೝՄΛߦ͏ϋϯυϥʔͷςετՄ ೳͳ࣮૷Λɺopenshift/osinͰߦͬͨ • ϥΠϒϥϦ͕ఏڙ͍ͯ͠ΔinterfaceΛ࣮૷ͨ͠ mockΛ࡞ΓɺςετΛͨ͠