-
Notifications
You must be signed in to change notification settings - Fork 0
GithubのSSO機能 #67
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
GithubのSSO機能 #67
Changes from all commits
f8899c3
0044f4d
aa8fc62
cd2a6e2
989f014
ad2f7cc
2ddf00a
35677c6
c0ac1e5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| /* | ||
| Copyright © 2023 NAME HERE <EMAIL ADDRESS> | ||
| */ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "sync" | ||
|
|
||
| "github.com/cocoide/commitify/internal/gateway" | ||
| "github.com/cocoide/commitify/internal/usecase" | ||
| "github.com/fatih/color" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| const ( | ||
| DeviceActivateURL = "https://github.com/login/device" | ||
| ) | ||
|
|
||
| var loginCmd = &cobra.Command{ | ||
| Use: "login", | ||
| Short: "login by github", | ||
| Long: `by login you can use auto pull request feature`, | ||
| Run: func(cmd *cobra.Command, args []string) { | ||
| httpClient := gateway.NewHttpClient() | ||
| u := usecase.NewLoginCmdUsecase(httpClient) | ||
| res, err := u.BeginGithubSSO() | ||
| if err != nil { | ||
| fmt.Printf("ログイン中にエラーが発生: %v", err) | ||
| } | ||
|
|
||
| var wg sync.WaitGroup | ||
| wg.Add(1) | ||
|
|
||
| errChan := make(chan error, 1) | ||
|
|
||
| go func() { | ||
| defer wg.Done() | ||
|
|
||
| req := &usecase.ScheduleVerifyAuthRequest{ | ||
| DeviceCode: res.DeviceCode, Interval: res.Interval, ExpiresIn: res.ExpiresIn} | ||
| err := u.ScheduleVerifyAuth(req) | ||
| errChan <- err | ||
| }() | ||
| fmt.Printf("以下のページで認証コード『%s』を入力して下さい。\n", res.UserCode) | ||
| fmt.Printf(color.HiCyanString("➡️ %s\n"), DeviceActivateURL) | ||
| wg.Wait() | ||
| err = <-errChan | ||
| if err != nil { | ||
| fmt.Printf("🚨認証エラーが発生: %v", err) | ||
| } else { | ||
| fmt.Printf("**🎉認証が正常に完了**\n") | ||
| } | ||
| }, | ||
| } | ||
|
|
||
| func init() { | ||
| rootCmd.AddCommand(loginCmd) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| package gateway | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "io" | ||
| "net/http" | ||
| "strconv" | ||
| ) | ||
|
|
||
| type HttpClient struct { | ||
| client *http.Client | ||
| endpoint string | ||
| headers map[string]string | ||
| params map[string]interface{} | ||
| body io.Reader | ||
| } | ||
|
|
||
| func NewHttpClient() *HttpClient { | ||
| return &HttpClient{ | ||
| client: &http.Client{}, | ||
| headers: make(map[string]string), | ||
| params: make(map[string]interface{}), | ||
| } | ||
| } | ||
|
|
||
| func (h *HttpClient) WithBaseURL(baseURL string) *HttpClient { | ||
| h.endpoint = baseURL | ||
| return h | ||
| } | ||
|
|
||
| func (h *HttpClient) WithBearerToken(token string) *HttpClient { | ||
| h.headers["Authorization"] = fmt.Sprintf("Bearer %s", token) | ||
| return h | ||
| } | ||
|
|
||
| func (h *HttpClient) WithPath(path string) *HttpClient { | ||
| h.endpoint = h.endpoint + "/" + path | ||
| return h | ||
| } | ||
|
|
||
| func (h *HttpClient) WithParam(key string, value interface{}) *HttpClient { | ||
| h.params[key] = value | ||
| return h | ||
| } | ||
|
|
||
| type HttpMethod int | ||
|
|
||
| const ( | ||
| GET HttpMethod = iota + 1 | ||
| POST | ||
| DELTE | ||
| PUT | ||
| ) | ||
|
|
||
| func (h *HttpClient) Execute(method HttpMethod) ([]byte, error) { | ||
| var methodName string | ||
| switch method { | ||
| case GET: | ||
| methodName = "GET" | ||
| case POST: | ||
| methodName = "POST" | ||
| case DELTE: | ||
| methodName = "DELETE" | ||
| case PUT: | ||
| methodName = "PUT" | ||
| } | ||
| client := h.client | ||
|
|
||
| req, err := http.NewRequest(methodName, h.endpoint, h.body) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| for k, v := range h.headers { | ||
| req.Header.Add(k, v) | ||
| } | ||
|
|
||
| query := req.URL.Query() | ||
| for key, value := range h.params { | ||
| switch v := value.(type) { | ||
| case string: | ||
| query.Add(key, v) | ||
| case int: | ||
| query.Add(key, strconv.Itoa(v)) | ||
| case bool: | ||
| query.Add(key, strconv.FormatBool(v)) | ||
| default: | ||
| return nil, fmt.Errorf("Failed to parse param value: %v", value) | ||
| } | ||
| } | ||
| req.URL.RawQuery = query.Encode() | ||
| resp, err := client.Do(req) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| body, err := io.ReadAll(resp.Body) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return body, nil | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| package usecase | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "github.com/cocoide/commitify/internal/entity" | ||
| "github.com/cocoide/commitify/internal/gateway" | ||
| "net/url" | ||
| "strconv" | ||
| "time" | ||
| ) | ||
|
|
||
| const ( | ||
| GithubClientID = "b27d87c28752d2363922" | ||
| GithubScope = "repo" | ||
| GrantType = "urn:ietf:params:oauth:grant-type:device_code" | ||
| ) | ||
|
|
||
| type LoginCmdUsecase struct { | ||
| http *gateway.HttpClient | ||
| } | ||
|
|
||
| func NewLoginCmdUsecase(http *gateway.HttpClient) *LoginCmdUsecase { | ||
| http.WithBaseURL("https://github.com/login") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. これ、フィールドに直接代入するのではなくて、
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. このコードは使用方法が少し不適切なんだけど |
||
| return &LoginCmdUsecase{http: http} | ||
| } | ||
|
|
||
| type BeginGithubSSOResponse struct { | ||
| DeviceCode string | ||
| UserCode string | ||
| Interval int | ||
| ExpiresIn int | ||
| } | ||
|
|
||
| func (u *LoginCmdUsecase) BeginGithubSSO() (*BeginGithubSSOResponse, error) { | ||
| b, err := u.http.WithPath("device/code"). | ||
| WithParam("client_id", GithubClientID). | ||
| WithParam("scope", GithubScope). | ||
| Execute(gateway.POST) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| values, err := url.ParseQuery(string(b)) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| deviceCode := values.Get("device_code") | ||
| userCode := values.Get("user_code") | ||
| expiresIn, err := strconv.Atoi(values.Get("expires_in")) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| interval, err := strconv.Atoi(values.Get("interval")) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if deviceCode == "" || userCode == "" { | ||
| return nil, fmt.Errorf("failed to parse code") | ||
| } | ||
| return &BeginGithubSSOResponse{ | ||
| DeviceCode: deviceCode, | ||
| UserCode: userCode, | ||
| ExpiresIn: expiresIn, | ||
| Interval: interval, | ||
| }, nil | ||
| } | ||
|
|
||
| type ScheduleVerifyAuthRequest struct { | ||
| DeviceCode string | ||
| Interval int | ||
| ExpiresIn int | ||
| } | ||
|
|
||
| func (u *LoginCmdUsecase) ScheduleVerifyAuth(req *ScheduleVerifyAuthRequest) error { | ||
| u.http = gateway.NewHttpClient(). | ||
| WithBaseURL("https://github.com/login"). | ||
| WithPath("oauth/access_token"). | ||
| WithParam("client_id", GithubClientID). | ||
| WithParam("device_code", req.DeviceCode). | ||
| WithParam("grant_type", GrantType) | ||
|
|
||
| timeout := time.After(time.Duration(req.ExpiresIn) * time.Second) | ||
| ticker := time.NewTicker(time.Duration(req.Interval) * time.Second) | ||
| defer ticker.Stop() | ||
|
|
||
| for { | ||
| select { | ||
| case <-timeout: | ||
| return fmt.Errorf("認証プロセスがタイムアウトしました") | ||
| case <-ticker.C: | ||
| b, err := u.http.Execute(gateway.POST) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| values, err := url.ParseQuery(string(b)) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| accessToken := values.Get("access_token") | ||
| if accessToken != "" { | ||
| config, err := entity.ReadConfig() | ||
| if err != nil { | ||
| return err | ||
| } | ||
| config.WithGithubToken(accessToken) | ||
| if err := config.WriteConfig(); err != nil { | ||
| return err | ||
| } | ||
| return nil | ||
| } | ||
| if newIntervalStr := values.Get("interval"); newIntervalStr != "" { | ||
| newInterval, err := strconv.Atoi(newIntervalStr) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| ticker.Stop() | ||
| ticker = time.NewTicker(time.Duration(newInterval) * time.Second) | ||
| } | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
リポジトリの情報までスコープに入れる必要あるかな?公開されているパブリック情報だけでも、UIDとかは持ってこれる気もするけど
https://docs.github.com/ja/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps#available-scopes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
たしかに、scopeはもう少し限定してもいいかもね
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
限定するというか、何も書かなければ最低限の情報しか取得しなくなるんじゃないかな。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
うんその認識だよ
(例)repo:〇〇で
pull requestを出すのに必要最低限のscopeにするってことだよね
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
prが可能かどうかのscopeがパッとせんから一旦これはrepoでよろしく🙏