Electron で Github OAuth トークンを取得する


wamw Friday I/O 6回目
株式会社ワムウでは、毎週金曜日は 興味がある事柄に取り組み、その成果を何らかの形でアウトプットする日としています。

今日は時間がなかったので、メモです。

## はじめに

下記コードは既存コードからメモ用に抜き出し、書き換えたものです。
これがそのまま完動するわけではないので、あくまで処理の流れをメモったものとなります。


## 前提


レンダラプロセスから何らかのアクションにてメインプロセスへ `connect-to-github` を送るとする。
その際メインプロセス側で下記のような形でアクセストークンを取得したい。

```typescript
// in main process

ipcMain.on('connect-to-github', async (event: Electron.Event) => {
  const credentials: GithubOAuthCredentials = {
    scopes: ['user', 'public_repo', 'repo'],
    client_id: process.env.GITHUB_CLIENT_ID || '',
    client_secret: process.env.GITHUB_CLIENT_SECRET || ''
  }

  const token: string = await getToken(credentials)
  event.sender.send('github-connected', token)
})

```

`GithubOAuthCredentials` はどこかで適当に。

```typescript
interface GithubOAuthCredentials {
  scopes: string[]
  client_id: string
  client_secret: string
}
```

## アクセストークン取得方法

アクセストークンを取得するためには `code` を取得する必要がある。
通常のWebアプリケーションの場合、アプリケーションの画面上でOAuth認証画面へ遷移し、認証後にリダイレクトURIへ遷移し返ってくる、という挙動から問題はないのだが、Electronの場合リダイレクトURIなどというものが存在しないため困ることになる。

これを解決するために「リダイレクトした、もしくはどこかの画面に遷移した際URIに `code` が存在したらその値を取得しウィンドウを閉じる」などの動きを作る必要がある。

上記の `getToken` は下記のような形だとする。

```typescript
export async function getToken(credentials: GithubOAuthCredentials): Promise<string> {
  const code = await getCode(credentials)
  const uri = `${accessTokenUri}?code=${code}&client_id=${credentials.client_id}&client_secret=${credentials.client_secret}`
  const headers = { 'Accept': 'application/json' }
  const response = await fetch(uri, { headers })
  const json = await response.json()
  const accessToken: string = json.access_token

  return accessToken
}
```

この場合の `getCode` は下記のような動きとなる。
`createOAuthWindow` は適当なウィンドウを作成する関数。
`getCode` が要求された場合、新規ウィンドウを立ち上げてそのウィンドウ上でGithubの認証を通す。
認証が通った後、リダイレクトがなされた場合、もしくは認証済みの場合単に遷移するのでその `code` を取得し、開いたウィンドウを閉じる。

```typescript
const oauthWindow = Electron.BrowserWindow | null

function getCode(credentials: GithubOAuthCredentials): Promise<string> {
  return new Promise((resolve, reject) => {
    if (!oauthWindow || oauthWindow.isDestroyed) {
      oauthWindow = createOAuthWindow()
      oauthWindow.on('close', () => {
        oauthWindow = null
      })
      oauthWindow.webContents.on('did-get-redirect-request', (event: Electron.Event, oldUrl: string, newUrl: string): void => {
        const code = extractCode(newUrl)
        if (code) {
        	oauthWindow.destroy()
        }
        resolve(code)
      })
      oauthWindow.webContents.on('will-navigate', (event: Electron.Event, url: string): void => {
        const code = extractCode(url)
        if (code) {
        	oauthWindow.destroy()
        }
        resolve(code)
      })
    }
    const uri = `${authorizeUri}?client_id=${credentials.client_id}&scope=${credentials.scopes}`
    oauthWindow.loadURL(uri)
    oauthWindow.show()
  })
}
```

`extractCode` はこんな感じ。

```typescript
function extractCode(url: string): string {
  const rawCode: RegExpExecArray | null = /code=([^&]*)/.exec(url) || null
  const code: string = (rawCode && rawCode.length > 1) ? rawCode[1] : ''
  const error: RegExpExecArray | null = /\?error=(.+)$/.exec(url) || null

  if (error) {
    throw new Error(error[1])
  }

  return code
}
```


## 参考

http://manos.im/blog/electron-oauth-with-github/