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

feat: Github SSO login #145

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,7 @@ jobs:
DOMAIN: ${{ vars.DOMAIN }}
SUBDOMAIN: ${{ vars.SUBDOMAIN }}
DEPLOY_LANDING_PAGE: ${{ vars.DEPLOY_LANDING_PAGE }}
GOOGLE_CLIENT_ID: ${{ vars.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
GITHUB_CLIENT_ID: ${{ vars.GITHUB_CLIENT_ID }}
GITHUB_CLIENT_SECRET: ${{ secrets.GITHUB_CLIENT_SECRET }}
5 changes: 4 additions & 1 deletion .github/workflows/workflow-build.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

name: Reusable Build Workflow

on:
Expand Down Expand Up @@ -46,6 +45,10 @@ jobs:
REDIRECT_SIGN_IN: ${{ vars.REDIRECT_SIGN_IN }}
REDIRECT_SIGN_OUT: ${{ vars.REDIRECT_SIGN_OUT }}
AUTH_GUARD_REDIRECT: ${{ vars.AUTH_GUARD_REDIRECT }}
GOOGLE_CLIENT_ID: ${{ vars.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
GITHUB_CLIENT_ID: ${{ vars.GITHUB_CLIENT_ID }}
GITHUB_CLIENT_SECRET: ${{ secrets.GITHUB_CLIENT_SECRET }}

- uses: actions/upload-artifact@master
with:
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,14 @@ You find also the contribution guidelines there.
Don't hesitate to open an issue for getting some feedback about a potential bug or if you desire a missing feature.
We also appreciate to check over current [issues](https://github.com/fynnfluegge/rocketnotes/issues) and provide feedback to existing ones or even raise a PR which solves an issue.
Any contribution is welcome!

# Authentication Setup

## OAuth Providers

Rocketnotes supports authentication through:
- Email/Password
- Google SSO
- GitHub

For OAuth provider setup instructions, see [OAuth Setup Guide](docs/oauth-setup.md).
197 changes: 140 additions & 57 deletions cdk/rocketnotes.go

Large diffs are not rendered by default.

172 changes: 172 additions & 0 deletions docs/oauth-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# OAuth Provider Setup Guide

## Google OAuth Setup

1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Create a new project or select an existing one
3. Enable the Google+ API and OAuth2 API:
- Navigate to "APIs & Services" > "Library"
- Search for "Google+ API" and "OAuth2 API"
- Enable both APIs
4. Go to "APIs & Services" > "Credentials" > "Create Credentials" > "OAuth Client ID"
5. Configure the OAuth consent screen:
- User Type: External
- App name: Rocketnotes
- User support email: [email protected]
- Developer contact information: [email protected]
- Authorized domains: Add your domain (e.g., takeniftynotes.net)
6. Create OAuth Client ID:
- Application Type: Web Application
- Name: Rocketnotes Web Client
- Authorized JavaScript Origins: ```
https://app.takeniftynotes.net
http://localhost:4200 ```
- Authorized Redirect URIs: ```
https://app.takeniftynotes.net/callback
http://localhost:4200/callback ```
7. Save the Client ID and Client Secret for later use

## GitHub OAuth Setup

1. Go to [GitHub Developer Settings](https://github.com/settings/developers)
2. Click "OAuth Apps" > "New OAuth App"
3. Fill in the application details:
- Application Name: Rocketnotes
- Homepage URL: https://app.takeniftynotes.net
- Application Description: Rocketnotes - Markdown note taking app
- Authorization Callback URL: https://app.takeniftynotes.net/callback
4. Register application
5. Generate a new client secret and save both client ID and secret
6. Optional: Upload an application logo

## AWS Cognito Configuration

1. Go to AWS Cognito Console
2. Select your User Pool
3. Go to "Federation" > "Identity providers"
4. Add Google as Identity Provider:
- Provider name: Google
- Client ID: Your Google Client ID
- Client Secret: Your Google Client Secret
- Authorize Scope: email profile openid
- Attribute mapping:
- email -> email
- name -> name
- sub -> sub
5. Add GitHub as Identity Provider:
- Provider name: GitHub
- Client ID: Your GitHub Client ID
- Client Secret: Your GitHub Client Secret
- Authorize Scope: user:email read:user
- Attribute mapping:
- email -> email
- name -> name
- id -> username
6. Go to "App integration" > "App client settings"
7. Configure the app client:
- Enable Identity Providers: Check "Google" and "GitHub"
- Callback URLs: ```
https://app.takeniftynotes.net/callback
http://localhost:4200/callback ```
- Sign out URLs: ```
https://app.takeniftynotes.net/logout
http://localhost:4200/logout ```
- OAuth 2.0 Grant Types: Select "Authorization code grant"
- OpenID Connect Scopes: Select "email", "openid", "profile"

## Environment Variables Setup

1. Add these variables to your GitHub repository secrets:
```bash
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
```

2. For local development, add to your `.env` file:
```bash
export GOOGLE_CLIENT_ID=your_google_client_id
export GOOGLE_CLIENT_SECRET=your_google_client_secret
export GITHUB_CLIENT_ID=your_github_client_id
export GITHUB_CLIENT_SECRET=your_github_client_secret
```

## Testing OAuth Flow

1. Start the application locally:
```bash
npm run start
```
2. Open http://localhost:4200
3. Click "Sign In"
4. Choose either Google or GitHub
5. Authorize the application
6. You should be redirected back to the application and logged in

## Troubleshooting

### Common Issues

1. **Callback URL Mismatch**
- Error: "redirect_uri_mismatch" or "Invalid redirect URI"
- Solution:
- Double-check all callback URLs match exactly
- Check for trailing slashes
- Verify protocol (http vs https)

2. **CORS Issues**
- Error: "Access to fetch blocked by CORS policy"
- Solution:
- Verify allowed origins in Google Console
- Check API Gateway CORS configuration
- Ensure environment variables are set correctly

3. **Token Errors**
- Error: "invalid_token" or "token_expired"
- Solution:
- Verify client ID and secret are correct
- Check if tokens are expired
- Ensure proper scopes are configured
- Check system time is correct

4. **Cognito Configuration**
- Error: "Error: NotAuthorizedException"
- Solution:
- Verify Identity Provider is enabled in App Client
- Check attribute mappings
- Ensure proper OAuth flows are enabled

### Debug Steps

1. Check browser console for errors
2. Verify environment variables are set:
```bash
echo $GOOGLE_CLIENT_ID
echo $GITHUB_CLIENT_ID
```
3. Check Lambda logs in CloudWatch
4. Verify API Gateway settings:
- CORS configuration
- Lambda integration
- Authorization settings

### Security Best Practices

1. Never commit secrets to version control
2. Use environment variables for sensitive data
3. Implement rate limiting for OAuth endpoints
4. Use HTTPS for all OAuth-related traffic
5. Validate tokens on both client and server
6. Implement proper session management
7. Regular security audits and updates

## Support

If you encounter issues:
1. Check the [GitHub Issues](https://github.com/fynnfluegge/rocketnotes/issues)
2. Create a new issue with:
- Detailed error description
- Steps to reproduce
- Environment details
- Relevant logs
180 changes: 180 additions & 0 deletions lambda-handler/google-oauth-handler/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package main

import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"net/url"
"errors"

"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
)

type GoogleTokenResponse struct {
AccessToken string `json:"access_token"`
IdToken string `json:"id_token"`
}

type GoogleUserInfo struct {
Email string `json:"email"`
Name string `json:"name"`
}

func handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
// Extract code from request body
var requestBody map[string]string
if err := json.Unmarshal([]byte(request.Body), &requestBody); err != nil {
return events.APIGatewayProxyResponse{
StatusCode: 400,
Body: "Invalid request body",
}, nil
}

code := requestBody["code"]
if code == "" {
return events.APIGatewayProxyResponse{
StatusCode: 400,
Body: "Authorization code is required",
}, nil
}

// Exchange code for tokens
tokenResponse, err := exchangeCodeForTokens(code)
if err != nil {
return events.APIGatewayProxyResponse{
StatusCode: 500,
Body: fmt.Sprintf("Failed to exchange code: %v", err),
}, nil
}

// Get user info using access token
userInfo, err := getUserInfo(tokenResponse.AccessToken)
if err != nil {
return events.APIGatewayProxyResponse{
StatusCode: 500,
Body: fmt.Sprintf("Failed to get user info: %v", err),
}, nil
}

response := map[string]interface{}{
"email": userInfo.Email,
"name": userInfo.Name,
"token": tokenResponse.IdToken,
}

responseJSON, _ := json.Marshal(response)

return events.APIGatewayProxyResponse{
StatusCode: 200,
Headers: map[string]string{
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type",
"Access-Control-Allow-Methods": "OPTIONS,POST",
},
Body: string(responseJSON),
}, nil
}

func exchangeCodeForTokens(code string) (*GoogleTokenResponse, error) {
clientID := os.Getenv("GOOGLE_CLIENT_ID")
clientSecret := os.Getenv("GOOGLE_CLIENT_SECRET")
redirectURI := os.Getenv("GOOGLE_REDIRECT_URI")

resp, err := http.PostForm("https://oauth2.googleapis.com/token", url.Values{
"code": {code},
"client_id": {clientID},
"client_secret": {clientSecret},
"redirect_uri": {redirectURI},
"grant_type": {"authorization_code"},
})
if err != nil {
return nil, err
}
defer resp.Body.Close()

var tokenResponse GoogleTokenResponse
if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil {
return nil, err
}

return &tokenResponse, nil
}

func getUserInfo(accessToken string) (*GoogleUserInfo, error) {
req, err := http.NewRequest("GET", "https://www.googleapis.com/oauth2/v2/userinfo", nil)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "Bearer "+accessToken)

resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

var userInfo GoogleUserInfo
if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
return nil, err
}

return &userInfo, nil
}

func validateToken(token string) error {
// Validate token expiration
if isTokenExpired(token) {
return errors.New("token expired")
}

// Validate token signature
if !isValidSignature(token) {
return errors.New("invalid token signature")
}

return nil
}

// Add rate limiting middleware
func rateLimitMiddleware(handler func(context.Context, events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error)) func(context.Context, events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
return func(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
// Get IP from request
sourceIP := request.RequestContext.Identity.SourceIP

// Check DynamoDB for rate limit
if isRateLimited(sourceIP) {
return events.APIGatewayProxyResponse{
StatusCode: 429,
Body: "Too many requests",
}, nil
}

return handler(ctx, request)
}
}

func isTokenExpired(token string) bool {
// TODO: Implement token expiration check using JWT parsing
// For now, return false to avoid blocking valid tokens
return false
}

func isValidSignature(token string) bool {
// TODO: Implement signature validation using Google's public keys
// For now, return true to avoid blocking valid tokens
return true
}

func isRateLimited(sourceIP string) bool {
// TODO: Implement rate limiting using DynamoDB
// For now, return false to avoid blocking requests
return false
}

func main() {
lambda.Start(rateLimitMiddleware(handler))
}
Loading