-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: /api/login?provider=providername endpoint redirects user to log…
…in with specified provider
- Loading branch information
1 parent
2bf4a0f
commit dc7a3f8
Showing
11 changed files
with
311 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
node_modules/ | ||
server/src/http/**/*.js | ||
server/src/lib/**/*.js | ||
server/test/**/*.js | ||
*.jic | ||
.arc-env | ||
sam.json | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
@aws | ||
runtime nodejs12.x | ||
# memory 1152 | ||
# timeout 30 | ||
# concurrency 1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { | ||
ArchitectHttpRequestPayload, | ||
ArchitectHttpResponsePayload, | ||
} from "../../types/http" | ||
import * as arc from "@architect/functions" | ||
import login from "../../lib/architect/oauth/handlers/login" | ||
|
||
export const handler = arc.http.async(login) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
# OAuth Configuration Notes | ||
|
||
1. Add a comma-delimited list of provider names to environment variable `OAUTH_PROVIDERS`. For example, the following adds two providers to the arc staging environment: | ||
|
||
```sh | ||
arc env staging OAUTH_PROVIDERS 'GOOGLE,GITHUB' | ||
``` | ||
|
||
NOTE: THe provider names can be any thing. THey are used is a postfix in subsequent steps: | ||
|
||
2. Get a OAuth Client ID and OAuth Client Secret from the provider. Here are [instructions for Google](https://developers.google.com/identity/protocols/oauth2/openid-connect). | ||
Use the redirect url of `https://<API_GATEWAY_ID>.execute-api.<REGION>.amazonaws.com/<ENVIRONMENT?>/auth/redirection` like `https://o92pvgjal2.execute-api.us-west-2.amazonaws.com/staging/auth/redirection` | ||
|
||
3. Add environment variable for Client ID: | ||
The environment variable is named like `OAUTH_<PROVIDER_NAME>_CLIENT_ID` where\_<PROVIDER_NAME> ` is the name of the provider you used above. | ||
|
||
```sh | ||
arc env staging OAUTH_GOOGLE_CLIENT_ID '235329004997-qbmh6kacitf56jtscckadmvd0qu9sqi6.apps.googleusercontent.com' | ||
``` | ||
|
||
4. Add environment variable for Client Secret | ||
The environment variable is named like `OAUTH_<PROVIDER_NAME>_CLIENT_SECRET` where\_<PROVIDER_NAME> ` is the name of the provider you used above. | ||
|
||
```sh | ||
arc env staging OAUTH_GOOGLE_CLIENT_SECRET '3maj6RZcZ1UqcbRv2zV0I782' | ||
``` | ||
|
||
5. Add environment variable for OAuth 2 [Authorization Endpoint](https://tools.ietf.org/html/rfc6749#section-3.1): | ||
The environment variable is named like `OAUTH_<PROVIDER_NAME>_ENDPOINT_AUTH` where\_<PROVIDER_NAME> ` is the name of the provider you used above. | ||
|
||
```sh | ||
arc env staging OAUTH_GOOGLE_ENDPOINT_AUTH 'https://accounts.google.com/o/oauth2/v2/auth' | ||
``` | ||
|
||
6. Add environment variable for OAuth 2 [Token Endpoint](https://tools.ietf.org/html/rfc6749#section-3.2): | ||
The environment variable is named like `OAUTH_<PROVIDER_NAME>_ENDPOINT_TOKEN` where\_<PROVIDER_NAME> ` is the name of the provider you used above. | ||
|
||
```sh | ||
arc env staging OAUTH_GOOGLE_ENDPOINT_TOKEN 'https://github.com/login/oauth/access_token' | ||
``` | ||
|
||
## Known OAuth 2 Provider Endpoints | ||
|
||
|
||
- Docs: https://developers.google.com/identity/protocols/oauth2/web-server | ||
- Authorization Endpoint: https://accounts.google.com/o/oauth2/v2/auth | ||
- Token Endpoint: https://oauth2.googleapis.com/token | ||
|
||
### Github | ||
|
||
- Docs: https://docs.github.com/en/free-pro-team@latest/developers/apps/authorizing-oauth-apps | ||
- Authorization Endpoint: https://github.com/login/oauth/authorize | ||
- Token Endpoint: https://github.com/login/oauth/access_token |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
/** | ||
* Reads configuration for a OAuth provider. | ||
*/ | ||
export class OAuthProviderConfig { | ||
public constructor(private readonly providerName: string) { | ||
if (!providerName) throw new Error("providerName must be specified") | ||
} | ||
|
||
/** | ||
* Returns the name of the config setting for this provider. | ||
*/ | ||
public name(template: Config): string { | ||
return template.replace(PROVIDER_PLACEHOLDER, this.providerName) | ||
} | ||
|
||
/** | ||
* Returns the value of the config setting for this provider. | ||
*/ | ||
public value(template: Config): string { | ||
return process.env[this.name(template)] | ||
} | ||
|
||
public getMissingConfigNames(): Array<string> { | ||
const requiredConfigs = [ | ||
Config.ClientID, | ||
Config.ClientSecret, | ||
Config.AuthorizationEndpoint, | ||
Config.TokenEndpoint, | ||
Config.RedirectURL | ||
] | ||
const missing: Array<string> = [] | ||
for (const cname of requiredConfigs) { | ||
const val = this.value(cname) | ||
if (!val) { | ||
missing.push(this.name(cname)) | ||
} | ||
} | ||
if (missing.length > 0) return missing | ||
else return null | ||
} | ||
} | ||
|
||
export enum Config { | ||
AuthorizationEndpoint = "OAUTH_{{PROVIDER}}_ENDPOINT_AUTH", | ||
TokenEndpoint = "OAUTH_{{PROVIDER}}_ENDPOINT_TOKEN", | ||
ClientID = "OAUTH_{{PROVIDER}}_CLIENT_ID", | ||
ClientSecret = "OAUTH_{{PROVIDER}}_CLIENT_SECRET", | ||
RedirectURL = "OAUTH_{{PROVIDER}}_REDIRECT_URL", | ||
} | ||
|
||
const PROVIDER_PLACEHOLDER = "{{PROVIDER}}" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
This folder contains implementation for an Architect app to implement OAuth client logic to allow users to login via for OAuth 2 providers such as Google, GitHub, Azure AD, etc. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import { createMockRequest } from "../../../../../test/support/architect" | ||
import login from "./login" | ||
|
||
describe("login handler", () => { | ||
// preserve environment | ||
const OLD_ENV = process.env | ||
afterAll(() => { | ||
process.env = OLD_ENV | ||
}) | ||
|
||
afterEach(() => { | ||
process.env = OLD_ENV | ||
}) | ||
|
||
it("should fail if no provider is in query string", async () => { | ||
const req = createMockRequest() | ||
// NOTE: no query string for provider | ||
const res = await login(req) | ||
expect(res).toHaveProperty("statusCode", 400) | ||
expect(res).toHaveProperty( | ||
"json.message", | ||
expect.stringMatching(/provider/) | ||
) | ||
}) | ||
|
||
it("should ensure environment variables: ClientID", async () => { | ||
const req = createMockRequest() | ||
req.queryStringParameters["provider"] = "GOO" | ||
|
||
// note no environment variable for Client ID, Client Secret in | ||
const res = await login(req) | ||
expect(res).toHaveProperty("statusCode", 400) | ||
expect(res).toHaveProperty( | ||
"json.message", | ||
expect.stringMatching(/OAUTH_GOO_CLIENT_ID/) | ||
) | ||
}) | ||
|
||
it("should ensure environment variables: Client ID, Client Secret", async () => { | ||
const req = createMockRequest() | ||
req.queryStringParameters["provider"] = "GOO" | ||
|
||
// NOTE: environment variable for Client ID, but not Client Secret | ||
process.env.OAUTH_GOO_CLIENT_ID = "googcid" | ||
const res = await login(req) | ||
expect(res).toHaveProperty("statusCode", 400) | ||
expect(res).toHaveProperty( | ||
"json.message", | ||
expect.stringMatching(/AUTH_GOO_CLIENT_SECRET/) | ||
) | ||
}) | ||
|
||
it("should ensure environment variables: Client ID, Client Secret, Auth Endpoint", async () => { | ||
const req = createMockRequest() | ||
req.queryStringParameters["provider"] = "GOO" | ||
|
||
// NOTE: environment variable for Client ID, but not Client Secret | ||
process.env.OAUTH_GOO_CLIENT_ID = "googcid" | ||
process.env.OAUTH_GOO_CLIENT_SECRET = "googsec" | ||
const res = await login(req) | ||
expect(res).toHaveProperty("statusCode", 400) | ||
expect(res).toHaveProperty( | ||
"json.message", | ||
expect.stringMatching(/AUTH_GOO_ENDPOINT_AUTH/) | ||
) | ||
}) | ||
|
||
it("should ensure environment variables: Client ID, Client Secret, Auth Endpoint, Token Endpoint", async () => { | ||
const req = createMockRequest() | ||
req.queryStringParameters["provider"] = "GOO" | ||
|
||
// NOTE: environment variable for Client ID, but not Client Secret | ||
process.env.OAUTH_GOO_CLIENT_ID = "googcid" | ||
process.env.OAUTH_GOO_CLIENT_SECRET = "googsec" | ||
process.env.OAUTH_GOO_ENDPOINT_AUTH = "https://goo.foo/auth" | ||
const res = await login(req) | ||
expect(res).toHaveProperty("statusCode", 400) | ||
expect(res).toHaveProperty( | ||
"json.message", | ||
expect.stringMatching(/AUTH_GOO_ENDPOINT_TOKEN/) | ||
) | ||
}) | ||
|
||
it("should redirect", async () => { | ||
const req = createMockRequest() | ||
req.queryStringParameters["provider"] = "GOO" | ||
|
||
process.env.OAUTH_GOO_CLIENT_ID = "googcid" | ||
process.env.OAUTH_GOO_CLIENT_SECRET = "googsec" | ||
process.env.OAUTH_GOO_ENDPOINT_AUTH = "https://goo.foo/auth" | ||
process.env.OAUTH_GOO_ENDPOINT_TOKEN = "https://goo.foo/tok" | ||
process.env.OAUTH_GOO_REDIRECT_URL = "https://mysite/auth/redir/goo" | ||
const res = await login(req) | ||
expect(res).toHaveProperty("statusCode", 302) | ||
expect(res).toHaveProperty("headers.location") | ||
const location = new URL(res.headers.location) | ||
expect(location.searchParams.get("response_type")).toEqual("code") | ||
expect(location.searchParams.get("scope")).toEqual("profile email") | ||
expect(location.searchParams.get("client_id")).toEqual(process.env.OAUTH_GOO_CLIENT_ID) | ||
expect(location.searchParams.get("redirect_uri")).toEqual(process.env.OAUTH_GOO_REDIRECT_URL) | ||
// TODO: also implement state param (make sure at least exists) | ||
expect(location.searchParams.has("state")).toBeTruthy() | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { | ||
ArchitectHttpRequestPayload, | ||
ArchitectHttpResponsePayload, | ||
} from "../../../../types/http" | ||
import { OAuthProviderConfig, Config } from "../OAuthProviderConfig" | ||
|
||
export default async function login( | ||
req: ArchitectHttpRequestPayload | ||
): Promise<ArchitectHttpResponsePayload> { | ||
/** | ||
* This is where we start the login flow. Uses the following steps: | ||
* 1. Get the provider from query string (?provider=<provider name>) | ||
* 2. Ensure we have client id, secret, etc. in environment variables from provider name | ||
* 3. Build URL for provider's authorization endpoint and redirect the user there. | ||
*/ | ||
const provider = req.queryStringParameters["provider"] | ||
if (!provider) { | ||
return errorResponse("provider query string must be provided") | ||
} | ||
const conf = new OAuthProviderConfig(provider) | ||
const validationResponse = validateProviderConfig(conf) | ||
if (validationResponse) { | ||
return validationResponse | ||
} | ||
|
||
let authUrl: URL | ||
try { | ||
authUrl = new URL(conf.value(Config.AuthorizationEndpoint)) | ||
} catch (err) { | ||
return errorResponse(`the ${conf.name(Config.AuthorizationEndpoint)} value ${conf.value(Config.AuthorizationEndpoint)} is not a valid URL`) | ||
} | ||
authUrl.searchParams.append("response_type", "code") | ||
authUrl.searchParams.append("scope", "profile email") | ||
authUrl.searchParams.append("client_id", conf.value(Config.ClientID)) | ||
authUrl.searchParams.append("redirect_uri", conf.value(Config.RedirectURL)) | ||
|
||
return { | ||
statusCode: 302, | ||
headers: { | ||
location: authUrl.toString(), | ||
}, | ||
} | ||
} | ||
|
||
function validateProviderConfig( | ||
conf: OAuthProviderConfig | ||
): ArchitectHttpResponsePayload | null { | ||
const missingConfigs = conf.getMissingConfigNames() | ||
if (missingConfigs) { | ||
const joined = missingConfigs.join(", ") | ||
return errorResponse( | ||
`The following environment settings must be provided: ${joined}` | ||
) | ||
} | ||
} | ||
|
||
function errorResponse(errorMessage: string): ArchitectHttpResponsePayload { | ||
const HTTP_STATUS_ERROR = 400 | ||
return { | ||
statusCode: HTTP_STATUS_ERROR, | ||
json: { | ||
message: errorMessage, | ||
}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,9 @@ | |
}, | ||
{ | ||
"path": "client" | ||
}, | ||
{ | ||
"path": "." | ||
} | ||
], | ||
"settings": {} | ||
|