Skip to content

Commit

Permalink
handle github enterprise
Browse files Browse the repository at this point in the history
  • Loading branch information
mathieudutour committed Sep 29, 2017
1 parent a7068ea commit 60f7648
Show file tree
Hide file tree
Showing 9 changed files with 202 additions and 32 deletions.
24 changes: 10 additions & 14 deletions app/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,6 @@ import { uuid } from './uuid'

const username: () => Promise<string> = require('username')

const ClientID = process.env.TEST_ENV ? '' : __OAUTH_CLIENT_ID__
const ClientSecret = process.env.TEST_ENV ? '' : __OAUTH_SECRET__

if (!ClientID || !ClientID.length || !ClientSecret || !ClientSecret.length) {
log.warn(
`KACTUS_OAUTH_CLIENT_ID and/or KACTUS_OAUTH_CLIENT_SECRET is undefined. You won't be able to authenticate new users.`
)
}

/** The OAuth scopes we need. */
const Scopes = ['repo', 'user']

Expand Down Expand Up @@ -588,6 +579,8 @@ export type AuthorizationResponse =
*/
export async function createAuthorization(
endpoint: string,
client_id: string,
client_secret: string,
login: string,
password: string,
oneTimePassword: string | null
Expand All @@ -605,8 +598,8 @@ export async function createAuthorization(
'authorizations',
{
scopes: Scopes,
client_id: ClientID,
client_secret: ClientSecret,
client_id,
client_secret,
note: note,
note_url: NoteURL,
fingerprint: uuid(),
Expand Down Expand Up @@ -825,15 +818,18 @@ export function getAccountForEndpoint(

export function getOAuthAuthorizationURL(
endpoint: string,
clientId: string,
state: string
): string {
const urlBase = getHTMLURL(endpoint)
const scope = encodeURIComponent(Scopes.join(' '))
return `${urlBase}/login/oauth/authorize?client_id=${ClientID}&scope=${scope}&state=${state}`
return `${urlBase}/login/oauth/authorize?client_id=${clientId}&scope=${scope}&state=${state}`
}

export async function requestOAuthToken(
endpoint: string,
client_id: string,
client_secret: string,
state: string,
code: string
): Promise<string | null> {
Expand All @@ -845,8 +841,8 @@ export async function requestOAuthToken(
'POST',
'login/oauth/access_token',
{
client_id: ClientID,
client_secret: ClientSecret,
client_id,
client_secret,
code: code,
state: state,
}
Expand Down
8 changes: 6 additions & 2 deletions app/src/lib/dispatcher/dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -723,8 +723,12 @@ export class Dispatcher {
* If validation is successful the store will advance to the authentication
* step.
*/
public setSignInEndpoint(url: string): Promise<void> {
return this.appStore._setSignInEndpoint(url)
public setSignInEndpoint(
url: string,
clientId: string,
clientSecret: string
): Promise<void> {
return this.appStore._setSignInEndpoint(url, clientId, clientSecret)
}

/**
Expand Down
25 changes: 22 additions & 3 deletions app/src/lib/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { uuid } from './uuid'
interface IOAuthState {
readonly state: string
readonly endpoint: string
readonly clientId: string
readonly clientSecret: string
readonly resolve: (account: Account) => void
readonly reject: (error: Error) => void
}
Expand All @@ -22,14 +24,29 @@ let oauthState: IOAuthState | null = null
* Note that the promise may not complete if the user doesn't complete the OAuth
* flow.
*/
export function askUserToOAuth(endpoint: string) {
export function askUserToOAuth(
endpoint: string,
clientId: string,
clientSecret: string
) {
// Disable the lint warning since we're storing the `resolve` and `reject`
// functions.
// tslint:disable-next-line:promise-must-complete
return new Promise<Account>((resolve, reject) => {
oauthState = { state: uuid(), endpoint, resolve, reject }
oauthState = {
state: uuid(),
endpoint,
clientId,
clientSecret,
resolve,
reject,
}

const oauthURL = getOAuthAuthorizationURL(endpoint, oauthState.state)
const oauthURL = getOAuthAuthorizationURL(
endpoint,
clientId,
oauthState.state
)
shell.openExternal(oauthURL)
})
}
Expand All @@ -49,6 +66,8 @@ export async function requestAuthenticatedUser(

const token = await requestOAuthToken(
oauthState.endpoint,
oauthState.clientId,
oauthState.clientSecret,
oauthState.state,
code
)
Expand Down
8 changes: 6 additions & 2 deletions app/src/lib/stores/app-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2844,8 +2844,12 @@ export class AppStore {
return Promise.resolve()
}

public _setSignInEndpoint(url: string): Promise<void> {
return this.signInStore.setEndpoint(url)
public _setSignInEndpoint(
url: string,
clientId: string,
clientSecret: string
): Promise<void> {
return this.signInStore.setEndpoint(url, clientId, clientSecret)
}

public _setSignInCredentials(
Expand Down
44 changes: 40 additions & 4 deletions app/src/lib/stores/sign-in-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ import { AuthenticationMode } from '../../lib/2fa'

import { minimumSupportedEnterpriseVersion } from '../../lib/enterprise'

const ClientID = process.env.TEST_ENV ? '' : __OAUTH_CLIENT_ID__!
const ClientSecret = process.env.TEST_ENV ? '' : __OAUTH_SECRET__!

if (!ClientID || !ClientID.length || !ClientSecret || !ClientSecret.length) {
log.warn(
`KACTUS_OAUTH_CLIENT_ID and/or KACTUS_OAUTH_CLIENT_SECRET is undefined. You won't be able to authenticate new users.`
)
}

function getUnverifiedUserErrorMessage(login: string): string {
return `Unable to authenticate. The account ${login} is lacking a verified email address. Please sign in to GitHub.com, confirm your email address in the Emails section under Personal settings, and try again.`
}
Expand Down Expand Up @@ -105,6 +114,8 @@ export interface IAuthenticationState extends ISignInState {
* URL when signing in against a GitHub Enterprise instance.
*/
readonly endpoint: string
readonly clientId: string
readonly clientSecret: string

/**
* A value indicating whether or not the endpoint supports
Expand Down Expand Up @@ -137,6 +148,8 @@ export interface ITwoFactorAuthenticationState extends ISignInState {
* URL when signing in against a GitHub Enterprise instance.
*/
readonly endpoint: string
readonly clientId: string
readonly clientSecret: string

/**
* The username specified by the user in the preceeding
Expand Down Expand Up @@ -266,6 +279,8 @@ export class SignInStore {
this.setState({
kind: SignInStep.Authentication,
endpoint,
clientId: ClientID,
clientSecret: ClientSecret,
supportsBasicAuth: true,
error: null,
loading: false,
Expand Down Expand Up @@ -299,13 +314,20 @@ export class SignInStore {
)
}

const endpoint = currentState.endpoint
const { endpoint, clientId, clientSecret } = currentState

this.setState({ ...currentState, loading: true })

let response: AuthorizationResponse
try {
response = await createAuthorization(endpoint, username, password, null)
response = await createAuthorization(
endpoint,
clientId,
clientSecret,
username,
password,
null
)
} catch (e) {
this.emitError(e)
return
Expand Down Expand Up @@ -334,6 +356,8 @@ export class SignInStore {
this.setState({
kind: SignInStep.TwoFactorAuthentication,
endpoint,
clientId: ClientID,
clientSecret: ClientSecret,
username,
password,
type: response.type,
Expand Down Expand Up @@ -411,7 +435,11 @@ export class SignInStore {

let account: Account
try {
account = await askUserToOAuth(currentState.endpoint)
account = await askUserToOAuth(
currentState.endpoint,
currentState.clientId,
currentState.clientSecret
)
} catch (e) {
this.setState({ ...currentState, error: e, loading: false })
return
Expand Down Expand Up @@ -453,7 +481,11 @@ export class SignInStore {
* If validation is successful the store will advance to the authentication
* step.
*/
public async setEndpoint(url: string): Promise<void> {
public async setEndpoint(
url: string,
clientId: string,
clientSecret: string
): Promise<void> {
const currentState = this.state

if (!currentState || currentState.kind !== SignInStep.EndpointEntry) {
Expand Down Expand Up @@ -496,6 +528,8 @@ export class SignInStore {
this.setState({
kind: SignInStep.Authentication,
endpoint,
clientId,
clientSecret,
supportsBasicAuth,
error: null,
loading: false,
Expand Down Expand Up @@ -547,6 +581,8 @@ export class SignInStore {
try {
response = await createAuthorization(
currentState.endpoint,
currentState.clientId,
currentState.clientSecret,
currentState.username,
currentState.password,
otp
Expand Down
38 changes: 35 additions & 3 deletions app/src/ui/lib/enterprise-server-entry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,20 @@ interface IEnterpriseServerEntryProps {
* endpoint url and submitted it either by clicking on the submit
* button or by submitting the form through other means (ie hitting Enter).
*/
readonly onSubmit: (url: string) => void
readonly onSubmit: (
url: string,
clientId: string,
clientSecret: string
) => void

/** An array of additional buttons to render after the "Continue" button. */
readonly additionalButtons?: ReadonlyArray<JSX.Element>
}

interface IEnterpriseServerEntryState {
readonly serverAddress: string
readonly clientId: string
readonly clientSecret: string
}

/** An entry form for an Enterprise server address. */
Expand All @@ -43,7 +49,7 @@ export class EnterpriseServerEntry extends React.Component<
> {
public constructor(props: IEnterpriseServerEntryProps) {
super(props)
this.state = { serverAddress: '' }
this.state = { serverAddress: '', clientId: '', clientSecret: '' }
}

public render() {
Expand All @@ -61,6 +67,20 @@ export class EnterpriseServerEntry extends React.Component<
placeholder="https://github.example.com"
/>

<TextBox
label="Application Client Id"
disabled={disableEntry}
onValueChanged={this.onclientIdChanged}
placeholder=""
/>

<TextBox
label="Application Client Secret"
disabled={disableEntry}
onValueChanged={this.onClientSecretChanged}
placeholder=""
/>

{this.props.error ? <Errors>{this.props.error.message}</Errors> : null}

<div className="actions">
Expand All @@ -77,7 +97,19 @@ export class EnterpriseServerEntry extends React.Component<
this.setState({ serverAddress })
}

private onclientIdChanged = (clientId: string) => {
this.setState({ clientId })
}

private onClientSecretChanged = (clientSecret: string) => {
this.setState({ clientSecret })
}

private onSubmit = () => {
this.props.onSubmit(this.state.serverAddress)
this.props.onSubmit(
this.state.serverAddress,
this.state.clientId,
this.state.clientSecret
)
}
}
8 changes: 6 additions & 2 deletions app/src/ui/lib/sign-in.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,12 @@ interface ISignInProps {
* Provide `children` elements to render additional buttons in the active form.
*/
export class SignIn extends React.Component<ISignInProps, {}> {
private onEndpointEntered = (url: string) => {
this.props.dispatcher.setSignInEndpoint(url)
private onEndpointEntered = (
url: string,
clientId: string,
clientSecret: string
) => {
this.props.dispatcher.setSignInEndpoint(url, clientId, clientSecret)
}

private onCredentialsEntered = (username: string, password: string) => {
Expand Down
Loading

0 comments on commit 60f7648

Please sign in to comment.