Skip to content
Closed
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
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,51 @@ A list of changes can be seen in the [CHANGELOG](CHANGELOG.md).

![Sign In Page](https://cloud.githubusercontent.com/assets/45028/4970624/7feb7dd8-6886-11e4-93e0-c9904af44ea8.png)

## Epitech Fork

This fork adds support for an Epitech provider, based on AzureAD and Epitech Intranet groups.

Enjoy ;)

### Build

```
docker build -t samber/epitech-oauth2-proxy:4.0.0 .
```

### Setup

1- Register a new webapp here => https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app

2- Copy Tenant ID + Client ID

3- Navigate to the "Certificates & secrets" page and create a new "client secret".

4- Navigate to "Authentication" page and set the redirect url to `http://localhost:80/oauth2/callback`

5- Navigate to "Manifest" page and set `groupMembershipClaims` to `All`.

```
docker run -d -p 80:80 -p 443:443 \
samber/epitech-oauth2-proxy \
-upstream=http://very-private-webapp:80 \
-http-address=0.0.0.0:80 \
-redirect-url=http://localhost:80/oauth2/callback \
-scope='profile User.Read' \
-email-domain=* \
-cookie-domain=localhost \
-cookie-secure=false \
-cookie-secret=somerandomstring1234567890 \
-provider=epitech \
-azure-tenant ******************************** \
-client-id ****************************** \
-client-secret '****************************' \
-epitech-group adm -epitech-group dpr -epitech-group dpra -epitech-group ape \
-epitech-auth-token auth-***********************
```

⚠️ For prod environment, remove the `-cookie-secure=false` argument ;)

## Installation

1. Choose how to deploy:
Expand Down
2 changes: 1 addition & 1 deletion docs/Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ GEM
thread_safe (0.3.6)
typhoeus (1.3.1)
ethon (>= 0.9.0)
tzinfo (1.2.5)
tzinfo (1.2.10)
thread_safe (~> 0.1)
unicode-display_width (1.4.1)

Expand Down
7 changes: 7 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func main() {
skipAuthRegex := StringArray{}
jwtIssuers := StringArray{}
googleGroups := StringArray{}
epitechGroups := StringArray{} // CUSTOM EPITECH
redisSentinelConnectionURLs := StringArray{}

config := flagSet.String("config", "", "path to config file")
Expand Down Expand Up @@ -129,6 +130,12 @@ func main() {
flagSet.String("pubjwk-url", "", "JWK pubkey access endpoint: required by login.gov")
flagSet.Bool("gcp-healthchecks", false, "Enable GCP/GKE healthcheck endpoints")

/*
* CUSTOM EPITECH
*/
flagSet.String("epitech-auth-token", "", "Auto-login token (format: auth-*****************).")
flagSet.Var(&epitechGroups, "epitech-group", "restrict logins to members of this Epitech Intranet group (may be given multiple times).")

flagSet.Parse(os.Args[1:])

if *showVersion {
Expand Down
8 changes: 8 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@ type Options struct {
signatureData *SignatureData
oidcVerifier *oidc.IDTokenVerifier
jwtBearerVerifiers []*oidc.IDTokenVerifier

/*
* CUSTOM EPITECH
*/
EpitechAuthToken string `flag:"epitech-auth-token" cfg:"epitech_auth_token" env:"OAUTH2_PROXY_EPITECH_AUTH_TOKEN"`
EpitechGroups []string `flag:"epitech-group" cfg:"epitech_group" env:"OAUTH2_PROXY_EPITECH_GROUPS"`
}

// SignatureData holds hmacauth signature hash and key
Expand Down Expand Up @@ -394,6 +400,8 @@ func parseProviderInfo(o *Options, msgs []string) []string {

o.provider = providers.New(o.Provider, p)
switch p := o.provider.(type) {
case *providers.EpitechProvider:
p.Configure(o.AzureTenant, o.EpitechAuthToken, o.EpitechGroups)
case *providers.AzureProvider:
p.Configure(o.AzureTenant)
case *providers.GitHubProvider:
Expand Down
109 changes: 109 additions & 0 deletions pkg/http_cache/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package http_cache

import (
"bufio"
"bytes"
"errors"
"net/http"
"net/http/httputil"
"sync"
"time"
)

func NewCacheTransport(originalTransport http.RoundTripper, seconds int) *CacheTransport {
transport := &CacheTransport{
data: make(map[string]string),
originalTransport: originalTransport,
}

cacheClearJob(seconds, transport)

return transport
}

func cacheClearJob(seconds int, transport *CacheTransport) {

interval := time.Duration(seconds) * time.Second
ticker := time.NewTicker(interval)

go func() {
for {

select {
case <-ticker.C:
transport.Clear()
}
}
}()
}

func cacheKey(r *http.Request) string {
return r.URL.String()
}

type CacheTransport struct {
data map[string]string
mu sync.RWMutex
originalTransport http.RoundTripper
}

func (c *CacheTransport) Set(r *http.Request, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[cacheKey(r)] = value
}

func (c *CacheTransport) Get(r *http.Request) (string, error) {
c.mu.RLock()
defer c.mu.RUnlock()

if val, ok := c.data[cacheKey(r)]; ok {
return val, nil
}

return "", errors.New("key not found in cache")
}

// Here is the main functionality
func (c *CacheTransport) RoundTrip(r *http.Request) (*http.Response, error) {

// Check if we have the response cached..
// If yes, we don't have to hit the server
// We just return it as is from the cache store.
if val, err := c.Get(r); err == nil {
return cachedResponse([]byte(val), r)
}

// Ok, we don't have the response cached, the store was probably cleared.
// Make the request to the server.
resp, err := c.originalTransport.RoundTrip(r)

if err != nil {
return nil, err
}

// Get the body of the response so we can save it in the cache for the next request.
buf, err := httputil.DumpResponse(resp, true)

if err != nil {
return nil, err
}

// Saving it to the cache store
c.Set(r, string(buf))

return resp, nil
}

func (c *CacheTransport) Clear() error {
c.mu.Lock()
defer c.mu.Unlock()

c.data = make(map[string]string)
return nil
}

func cachedResponse(b []byte, r *http.Request) (*http.Response, error) {
buf := bytes.NewBuffer(b)
return http.ReadResponse(bufio.NewReader(buf), r)
}
181 changes: 181 additions & 0 deletions providers/epitech.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package providers

import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"

"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/http_cache"
)

// EpitechProvider represents an Epitech Intranet based Identity Provider
type EpitechProvider struct {
*AzureProvider
AuthToken string

// GroupValidator is a function that determines if the passed email is in
// the configured Epitech group.
GroupValidator func(string) bool

client *http.Client
}

type epitechGroupMember struct {
Type string `json:"type"`
Login string `json:"login"`
Slug string `json:"slug"`
Location string `json:"location"`
Title string `json:"title"`
Close bool `json:"close"`
}

// NewEpitechProvider initiates a new EpitechProvider
func NewEpitechProvider(p *ProviderData) *EpitechProvider {
provider := &EpitechProvider{
AzureProvider: NewAzureProvider(p),

// Set a default GroupValidator to just always return valid (true), it will
// be overwritten if we configured a Epitech group restriction.
GroupValidator: func(email string) bool {
return true
},

//Create a custom client so we can make use of our RoundTripper
//If you make use of http.Get(), the default http client located at http.DefaultClient is used instead
//Since we have special needs, we have to make use of our own http.RoundTripper implementation
client: &http.Client{
Transport: http_cache.NewCacheTransport(http.DefaultTransport, 60),
Timeout: time.Second * 5,
},
}

provider.ProviderName = "Epitech"

return provider
}

// Configure defaults the EpitechProvider configuration options
func (p *EpitechProvider) Configure(tenant string, authToken string, groups []string) {
p.AzureProvider.Configure(tenant)
p.AuthToken = authToken

if len(groups) > 0 {
p.GroupValidator = func(email string) bool {
return p.verifyGroupMembership(groups, authToken, email)
}
}
}

// GetEmailAddress returns the Account email address
func (p *EpitechProvider) GetEmailAddress(s *sessions.SessionState) (string, error) {
return p.AzureProvider.GetEmailAddress(s)
}

// GetUserName returns the Account email address
func (p *EpitechProvider) GetUserName(s *sessions.SessionState) (string, error) {
return p.AzureProvider.GetEmailAddress(s)
}

func (p *EpitechProvider) Redeem(redirectURL, code string) (s *sessions.SessionState, err error) {
s, err = p.AzureProvider.Redeem(redirectURL, code)
if err != nil {
return
}

// commented for now (probably too much data for X-Forwarded-User header)
// email, err := p.GetEmailAddress(s)
// if err != nil {
// return nil, err
// }
// rawUser, err := p.getEpitechUser(email, p.AuthToken)
// if err != nil {
// return nil, err
// }
// s.User = rawUser

return
}

// ValidateGroup validates that the provided email exists in the configured Epitech
// group(s).
func (p *EpitechProvider) ValidateGroup(email string) bool {
return p.GroupValidator(email)
}

func (p *EpitechProvider) verifyGroupMembership(groups []string, authToken string, email string) bool {
fmt.Println("Checking Epitech group membership for " + email)

for _, group := range groups {
members, err := p.getEpitechGroupMembers(group, authToken)
if err != nil {
fmt.Println(err.Error())
continue
}

for _, member := range members {
if member.Slug == email || member.Login == email {
return true
}
}
}

return false
}

func (p *EpitechProvider) getEpitechGroupMembers(groupName string, authToken string) ([]epitechGroupMember, error) {
path := fmt.Sprintf("https://intra.epitech.eu/%s/group/%s/member?format=json", authToken, groupName)

var req *http.Request
req, err := http.NewRequest("GET", path, nil)
if err != nil {
return []epitechGroupMember{}, err
}

resp, err := p.client.Do(req)
if err != nil {
return []epitechGroupMember{}, err
}

var body []byte
body, err = ioutil.ReadAll(resp.Body)
resp.Body.Close()

if resp.StatusCode != 200 {
return []epitechGroupMember{}, err
}

var members []epitechGroupMember
err = json.Unmarshal([]byte(body), &members)
if err != nil {
return nil, err
}

return members, nil
}

func (p *EpitechProvider) getEpitechUser(email string, authToken string) (string, error) {
path := fmt.Sprintf("https://intra.epitech.eu/%s/user/%s/?format=json", authToken, email)

var req *http.Request
req, err := http.NewRequest("GET", path, nil)
if err != nil {
return "", err
}

resp, err := p.client.Do(req)
if err != nil {
return "", err
}

var body []byte
body, err = ioutil.ReadAll(resp.Body)
resp.Body.Close()

if resp.StatusCode != 200 {
return "", err
}
return string(body), nil
}
Loading