1. yumin

    No comment

    yumin
Changes in body
Source | HTML | Preview

この記事は実装は一部となります

図:

image.png

pkg:

import (
    "github.com/openshift/osin"
)

構造体:

// App ...
type App struct {
    OAuthServer  *osin.Server 
    DBClient     db.DB 
}

DB周り (ID/Password認証情報など、実装が必要)

// DB ...
type DB interface {
    Close()
    CheckIDPassword(ID,Password) error
    CheckIDByFefresToken(refreshToken string) error
    //...
}

// ...
// 各メソッドの実装

Storage周り (accesstoken/refreshtokenの情報など、実装が必要)

image.png
https://github.com/openshift/osin#storage-backends

osin/storage.go
// Storage interface
type Storage interface {
    // Clone the storage if needed. For example, using mgo, you can clone the session with session.Clone
    // to avoid concurrent access problems.
    // This is to avoid cloning the connection at each method access.
    // Can return itself if not a problem.
    Clone() Storage

    // Close the resources the Storage potentially holds (using Clone for example)
    Close()

    // GetClient loads the client by id (client_id)
    GetClient(id string) (Client, error)

    // SaveAuthorize saves authorize data.
    SaveAuthorize(*AuthorizeData) error

    // LoadAuthorize looks up AuthorizeData by a code.
    // Client information MUST be loaded together.
    // Optionally can return error if expired.
    LoadAuthorize(code string) (*AuthorizeData, error)

    // RemoveAuthorize revokes or deletes the authorization code.
    RemoveAuthorize(code string) error

    // SaveAccess writes AccessData.
    // If RefreshToken is not blank, it must save in a way that can be loaded using LoadRefresh.
    SaveAccess(*AccessData) error

    // LoadAccess retrieves access data by token. Client information MUST be loaded together.
    // AuthorizeData and AccessData DON'T NEED to be loaded if not easily available.
    // Optionally can return error if expired.
    LoadAccess(token string) (*AccessData, error)

    // RemoveAccess revokes or deletes an AccessData.
    RemoveAccess(token string) error

    // LoadRefresh retrieves refresh AccessData. Client information MUST be loaded together.
    // AuthorizeData and AccessData DON'T NEED to be loaded if not easily available.
    // Optionally can return error if expired.
    LoadRefresh(token string) (*AccessData, error)

    // RemoveRefresh revokes or deletes refresh AccessData.
    RemoveRefresh(token string) error
}

authorize_code 認証エンドポイント:

// Authorize ...
func (a *App) Authorize(w http.ResponseWriter, r *http.Request) {
    resp := a.OAuthServer.NewResponse()
    defer resp.Close()
    if ar := a.OAuthServer.HandleAuthorizeRequest(resp, r); ar != nil {
        id := r.FormValue("id")
        password := r.FormValue("password")

        //csrftoken checkなど
        //...

        //DBにUserとPassword確認
        err := a.DBClient.CheckIDPassword(id, password)
        if err == sql.ErrNoRows {

            // NoRows場合の処理

        } else if err != nil {

            // error処理

        }
        ar.Authorized = true
        a.OAuthServer.FinishAuthorizeRequest(resp, r, ar)
        osin.OutputJSON(resp, w, r)
    }
}

実際使ってるメソッドはライブラリーのHandleAuthorizeRequestメソッドなので、中身:

HandleAuthorizeRequestの中身(一部分省略)

osin/authorize.go
// authorization requests
func (s *Server) HandleAuthorizeRequest(w *Response, r *http.Request) *AuthorizeRequest {
    r.ParseForm()

    // create the authorization request 
    unescapedUri, err := url.QueryUnescape(r.FormValue("redirect_uri"))
    if err != nil {
        w.SetErrorState(E_INVALID_REQUEST, "", "")
        w.InternalError = err
        return nil
    }

    // ...

    ret := &AuthorizeRequest{
        State:       r.FormValue("state"),
        Scope:       r.FormValue("scope"),
        //...
    }

    // must have a valid client
    ret.Client, err = w.Storage.GetClient(r.FormValue("client_id"))
    if err == ErrNotFound {
        w.SetErrorState(E_UNAUTHORIZED_CLIENT, "", ret.State)
        return nil
    }

    // ...

    requestType := AuthorizeRequestType(r.FormValue("response_type"))
    if s.Config.AllowedAuthorizeTypes.Exists(requestType) {
        switch requestType {
        case CODE:
            // ...
        case TOKEN:
            // ...
        }
        return ret
    }
    //...
}

パラメターについて

//response_type 必須 
アクセストークンを要求する場合は "token" を設定します認可コードの場合は "code" を設定します 

//client_id  必須 
クライアントを設定します 

//redirect_uri 条件付き必須  
クライアントと認可サーバ間で事前にリダイレクト先の URI が決められていない場合は必須になります 
エンドユーザによる認可が完了した際に認可サーバがユーザーエージェントをリダイレクトする先の絶対 URI を設定します

//scope 任意 
リクエストで要求するアクセス権のスコープを空白区切りの文字列で指定します scopeの値に何を指定するかは認可サーバによって定義
されます 空白区切りで複数の値を含む場合はその順序に意味はありませんそれぞれの値を合わせた範囲が要求するアクセス権になります

//state 任意
リクエストから完了時のコールバックまでの間クライアントが状態管理できる値を指定します 
認可サーバは指定された値をそのままリダイレクトURIにユーザーエージェントをリダイレクトします 

Accesstoken、refreshTokenの発行、保存など

// Token ...
func (a *App) Token(w http.ResponseWriter, r *http.Request) {
    resp := a.OAuthServer.NewResponse()
    defer resp.Close()
    if ar := a.OAuthServer.HandleAccessRequest(resp, r); ar != nil {
        switch ar.Type {
        case osin.AUTHORIZATION_CODE:
            ar.Authorized = true
        case osin.REFRESH_TOKEN:

       // DBにtokenと紐ついてるuserの存在(退会の場合あり)を確認
            err := a.DBClient.CheckIDByFefresToken(ar.UserData.(string))

            //  NoRows and error処理 ...

            ar.Authorized = true
        case osin.PASSWORD:

            // DBにUserとPassword確認
            err := a.DBClient.CheckIDPassword(ar.Username, ar.Password)

            //  NoRows and error処理 ...

            ar.Authorized = true
        }

        // FinishAccessRequestが全部やってくれた
        a.OAuthServer.FinishAccessRequest(resp, r, ar)
    }

    if resp.InternalError != nil {

        // error処理 ...

    }
    osin.OutputJSON(resp, w, r)
}

Accesstoken、refreshTokenの発行など FinishAccessRequestがやってくれた

osin/access.go
func (s *Server) FinishAccessRequest(w *Response, r *http.Request, ar *AccessRequest) {
    // don't process if is already an error
    if w.IsError {
        return
    }
    redirectUri := r.FormValue("redirect_uri")
    // Get redirect uri from AccessRequest if it's there (e.g., refresh token request)
    if ar.RedirectUri != "" {
        redirectUri = ar.RedirectUri
    }
    if ar.Authorized {
        var ret *AccessData
        var err error

        if ar.ForceAccessData == nil {
            // generate access token
            ret = &AccessData{
                Client:        ar.Client,
                AuthorizeData: ar.AuthorizeData,
                AccessData:    ar.AccessData,
                RedirectUri:   redirectUri,
                CreatedAt:     s.Now(),
                ExpiresIn:     ar.Expiration,
                UserData:      ar.UserData,
                Scope:         ar.Scope,
            }

            // accesstoken and refreshToken ここで発行する
            ret.AccessToken, ret.RefreshToken, err = s.AccessTokenGen.GenerateAccessToken(ret, ar.GenerateRefresh)
            if err != nil {
                s.setErrorAndLog(w, E_SERVER_ERROR, err, "finish_access_request=%s", "error generating token")
                return
            }
        } else {
            ret = ar.ForceAccessData
        }

        // DBに保存までやってくれる、インタフェース:Storegeの実装が必要
        if err = w.Storage.SaveAccess(ret); err != nil {
            s.setErrorAndLog(w, E_SERVER_ERROR, err, "finish_access_request=%s", "error saving access token")
            return
        }

        // ...

        // output data
        w.Output["access_token"] = ret.AccessToken
        w.Output["token_type"] = s.Config.TokenType
        w.Output["expires_in"] = ret.ExpiresIn
        if ret.RefreshToken != "" {
            w.Output["refresh_token"] = ret.RefreshToken
        }
        if ret.Scope != "" {
            w.Output["scope"] = ret.Scope
        }
    } else {
        s.setErrorAndLog(w, E_ACCESS_DENIED, nil, "finish_access_request=%s", "authorization failed")
    }
}

参考:

https://murashun.jp/blog/20150920-01.html
https://www.infoscoop.org/blogjp/2014/05/12/1858/