Skip to content
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

Add GitHub login with organization and group restrictions #609

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,34 @@ Curious about upcoming features? Check our [Roadmap](https://github.com/orgs/Vet
## License :scroll:

This project is under the MIT License - see the [License](https://github.com/Vets-Who-Code/vwc-site/blob/master/LICENSE) for more details.

## GitHub OAuth Setup Instructions

To authenticate users via GitHub and restrict access to members of the Vets Who Code organization, follow these steps:

1. **Create a GitHub OAuth App**: Go to your GitHub settings, navigate to Developer settings > OAuth Apps, and create a new OAuth app.
2. **Application Name**: Give your application a name that reflects your project.
3. **Homepage URL**: Enter the URL of your application.
4. **Authorization callback URL**: This is critical. Enter `http://localhost:3000/api/auth/callback/github` for development. Adjust the domain accordingly for production.
5. **Client ID & Client Secret**: Once the application is created, GitHub will provide a Client ID and a Client Secret. Keep these confidential.

Add the Client ID and Client Secret to your `.env.local` file:

```plaintext
GITHUB_ID=your-github-client-id
GITHUB_SECRET=your-github-client-secret
```

## Configuring Access Restrictions

To configure access restrictions based on organization and group membership, follow these steps:

1. **Verify Organization Membership**: Utilize the GitHub API to check if the authenticated user is a member of the Vets Who Code organization.
2. **Group-Based Access Control**: Further restrict access to users who are part of the "students" group within the Vets Who Code organization.
3. **Environment Variables**: Ensure you have the Vets Who Code GitHub organization ID in your `.env.local` file:

```plaintext
GITHUB_ORGANIZATION_ID=vets-who-code
```

These steps ensure that only authorized members of the Vets Who Code community can access certain parts of the application.
72 changes: 72 additions & 0 deletions components/forms/login-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { signIn, useSession } from "next-auth/react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import Button from "@ui/button";
import Input from "@ui/form-elements/input";
import Feedback from "@ui/form-elements/feedback";

const LoginForm = () => {
const { data: session } = useSession();
const [loginError, setLoginError] = useState("");
const {
register,
handleSubmit,
formState: { errors },
} = useForm();

const onSubmit = async (data) => {
const result = await signIn("github", {
redirect: false,
...data,
});

if (result?.error) {
setLoginError(result.error);
}
};

if (session) {
return (
<div>
<p>You are already logged in</p>
</div>
);
}

return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
{loginError && <Feedback state="error">{loginError}</Feedback>}
<div>
<label htmlFor="username" className="block text-sm font-medium text-gray-700">
Username
</label>
<Input
id="username"
type="text"
{...register("username", { required: "Username is required" })}
className="mt-1"
/>
{errors.username && <Feedback state="error">{errors.username.message}</Feedback>}
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
Password
</label>
<Input
id="password"
type="password"
{...register("password", { required: "Password is required" })}
className="mt-1"
/>
{errors.password && <Feedback state="error">{errors.password.message}</Feedback>}
</div>
<div>
<Button type="submit" className="w-full">
Sign in with GitHub
</Button>
</div>
</form>
);
};

export default LoginForm;
4 changes: 4 additions & 0 deletions env.local
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
NEXT_PUBLIC_GOOGLE_ANALYTICS_ID=G-WSXY307CRR
GITHUB_ID=your-github-client-id
GITHUB_SECRET=your-github-client-secret
NEXTAUTH_URL=http://localhost:3000
GITHUB_ORGANIZATION_ID=vets-who-code
28 changes: 28 additions & 0 deletions pages/api/auth/[...nextauth].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import NextAuth from "next-auth";
import GitHubProvider from "next-auth/providers/github";
import { checkOrganizationMembership, checkGroupMembership } from "./membership-utils";

export default NextAuth({
providers: [
GitHubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
],
callbacks: {
async signIn({ user, account, profile }) {
if (account.provider === "github") {
const isMember = await checkOrganizationMembership(account.accessToken);
if (!isMember) {
return false; // Not a member of the organization
}

const isInStudentsGroup = await checkGroupMembership(account.accessToken, user.id);
if (!isInStudentsGroup) {
return false; // Not in the "students" group
}
}
return true; // Sign in successful
},
},
});
46 changes: 46 additions & 0 deletions pages/api/auth/group-membership.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Octokit } from "@octokit/core";

const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });

export async function checkGroupMembership(userAccessToken, userId) {
try {
const orgs = await octokit.request("GET /user/orgs", {
headers: {
authorization: `token ${userAccessToken}`,
},
});

const vetsWhoCodeOrg = orgs.data.find(org => org.login === "Vets-Who-Code");

if (!vetsWhoCodeOrg) {
return false; // User is not part of the Vets Who Code organization
}

const teams = await octokit.request("GET /orgs/{org}/teams", {
org: vetsWhoCodeOrg.login,
headers: {
authorization: `token ${userAccessToken}`,
},
});

const studentsTeam = teams.data.find(team => team.name === "students");

if (!studentsTeam) {
return false; // "students" team does not exist within the organization
}

const membership = await octokit.request("GET /orgs/{org}/teams/{team_slug}/memberships/{username}", {
org: vetsWhoCodeOrg.login,
team_slug: studentsTeam.slug,
username: userId,
headers: {
authorization: `token ${userAccessToken}`,
},
});

return membership.data.state === "active";
} catch (error) {
console.error("Error checking group membership:", error);
return false;
}
}
25 changes: 25 additions & 0 deletions pages/api/auth/organization-membership.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Octokit } from "@octokit/core";

const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });

export async function checkOrganizationMembership(userAccessToken) {
try {
const response = await octokit.request("GET /user/memberships/orgs", {
headers: {
authorization: `token ${userAccessToken}`,
},
});

const isMember = response.data.some(
(membership) =>
membership.organization.login.toLowerCase() ===
"vets-who-code".toLowerCase() &&
membership.state === "active"
);

return isMember;
} catch (error) {
console.error("Error checking organization membership:", error);
return false;
}
}
23 changes: 9 additions & 14 deletions tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ module.exports = {
mandy: "#df5b6c",
tan: "#d2a98e",
mishcka: "#e2e2e8",
// Custom styles for the login page
login: {
background: "#f5f5f5",
inputBorder: "#e2e8f0",
inputFocusBorder: "#93c5fd",
buttonBackground: "#4f46e5",
buttonHoverBackground: "#4338ca",
buttonText: "#ffffff",
},
},
typography: ({ theme }) => ({
DEFAULT: {
Expand Down Expand Up @@ -174,29 +183,15 @@ module.exports = {
},
screens: {
maxSm: { max: "575px" },
// => @media (max-width: 575px) { ... }
maxXl: { max: "1199px" },
// => @media (max-width: 1199px) { ... }
maxLg: { max: "991px" },
// => @media (max-width: 991px) { ... }
smToMd: { min: "576px", max: "767px" },
sm: "576px",
// => @media (min-width: 576px) { ... }

md: "768px",
// => @media (min-width: 768px) { ... }

lg: "992px",
// => @media (min-width: 992px) { ... }

xl: "1200px",
// => @media (min-width: 1200px) { ... }

"2xl": "1400px",
// => @media (min-width: 1400px) { ... }

"3xl": "1600px",
// => @media (min-width: 1600px) { ... }
},
zIndex: {
1: 1,
Expand Down
Loading