Add supprt for new Rails 5.2 aes-256-gcm cookies

This commit is contained in:
Vikram Rangnekar 2019-04-10 01:38:48 -04:00
parent 96adec81bd
commit 512647156e
18 changed files with 571 additions and 265 deletions

View File

@ -35,7 +35,7 @@ RUN apk add --no-cache tzdata
COPY --from=go-build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=go-build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=go-build /app/service . COPY --from=go-build /app/service .
COPY --from=go-build /app/*.yml ./ COPY --from=go-build /app/config/*.yml ./
RUN chmod +x /app/service RUN chmod +x /app/service
USER nobody USER nobody

View File

@ -2,7 +2,7 @@ title: Super Graph Development
host_port: 0.0.0.0:8080 host_port: 0.0.0.0:8080
web_ui: true web_ui: true
debug_level: 1 debug_level: 1
enable_tracing: false enable_tracing: true
# Throw a 401 on auth failure for queries that need auth # Throw a 401 on auth failure for queries that need auth
# valid values: always, per_query, never # valid values: always, per_query, never
@ -25,28 +25,40 @@ auth_fail_block: never
# sheep: sheep # sheep: sheep
auth: auth:
type: header # Can be 'rails' or 'jwt'
type: rails
cookie: _app_session cookie: _app_session
# Comment this out if you want to disable setting
# the user_id via a header. Good for testing
header: X-User-ID header: X-User-ID
# rails_cookie: rails:
# secret_key_base: caf335bfcfdb04e50db5bb0a4d67ab9... # Rails version this is used for reading the
# various cookies formats.
version: 5.2
# rails_memcache: # Found in 'Rails.application.config.secret_key_base'
# host: 127.0.0.1 secret_key_base: 0a248500a64c01184edb4d7ad3a805488f8097ac761b76aaa6c17c01dcb7af03a2f18ba61b2868134b9c7b79a122bc0dadff4367414a2d173297bfea92be5566
# rails_redis: # Remote cookie store. (memcache or redis)
# url: redis://127.0.0.1:6379 # url: redis://127.0.0.1:6379
# password: "" # password: test
# max_idle: 80, # max_idle: 80,
# max_active: 12000, # max_active: 12000,
# In most cases you don't need these
# salt: "encrypted cookie"
# sign_salt: "signed encrypted cookie"
# auth_salt: "authenticated encrypted cookie"
# jwt: # jwt:
# provider: auth0 # provider: auth0
# secret: abc335bfcfdb04e50db5bb0a4d67ab9 # secret: abc335bfcfdb04e50db5bb0a4d67ab9
# public_key_file: /secrets/public_key.pem # public_key_file: /secrets/public_key.pem
# public_key_type: ecdsa #rsa # public_key_type: ecdsa #rsa
database: database:
type: postgres type: postgres
host: db host: db
@ -77,18 +89,24 @@ database:
fields: fields:
- name: users - name: users
# This filter will overwrite defaults.filter
filter: ["{ id: { eq: $user_id } }"] filter: ["{ id: { eq: $user_id } }"]
- name: products - name: products
# Multiple filters are AND'd together
filter: [ filter: [
"{ price: { gt: 0 } }", "{ price: { gt: 0 } }",
"{ price: { lt: 8 } }" "{ price: { lt: 8 } }"
] ]
- name: customers - name: customers
# No filter is used for this field not
# even defaults.filter
filter: none filter: none
- name: me - # You can create new fields that have a
# real db table backing them
name: me
table: users table: users
filter: ["{ id: { eq: $user_id } }"] filter: ["{ id: { eq: $user_id } }"]

113
config/prod.yml Normal file
View File

@ -0,0 +1,113 @@
title: Super Graph Production
host_port: 0.0.0.0:8080
web_ui: false
debug_level: 0
enable_tracing: false
# Throw a 401 on auth failure for queries that need auth
# valid values: always, per_query, never
auth_fail_block: always
# Postgres related environment Variables
# SG_DATABASE_HOST
# SG_DATABASE_PORT
# SG_DATABASE_USER
# SG_DATABASE_PASSWORD
# Auth related environment Variables
# SG_AUTH_RAILS_COOKIE_SECRET_KEY_BASE
# SG_AUTH_RAILS_REDIS_URL
# SG_AUTH_RAILS_REDIS_PASSWORD
# SG_AUTH_JWT_PUBLIC_KEY_FILE
# inflections:
# person: people
# sheep: sheep
auth:
# Can be 'rails' or 'jwt'
type: rails
cookie: _app_session
# Comment this out if you want to disable setting
# the user_id via a header. Good for testing
header: X-User-ID
rails:
# Rails version this is used for reading the
# various cookies formats.
version: 5.2
# Found in 'Rails.application.config.secret_key_base'
secret_key_base: 0a248500a64c01184edb4d7ad3a805488f8097ac761b76aaa6c17c01dcb7af03a2f18ba61b2868134b9c7b79a122bc0dadff4367414a2d173297bfea92be5566
# Remote cookie store. (memcache or redis)
# url: redis://127.0.0.1:6379
# password: test
# max_idle: 80,
# max_active: 12000,
# In most cases you don't need these
# salt: "encrypted cookie"
# sign_salt: "signed encrypted cookie"
# auth_salt: "authenticated encrypted cookie"
# jwt:
# provider: auth0
# secret: abc335bfcfdb04e50db5bb0a4d67ab9
# public_key_file: /secrets/public_key.pem
# public_key_type: ecdsa #rsa
database:
type: postgres
host: db
port: 5432
dbname: app_development
user: postgres
password: ''
#pool_size: 10
#max_retries: 0
#log_level: "debug"
# Define variables here that you want to use in filters
variables:
account_id: "select account_id from users where id = $user_id"
# Define defaults to for the field key and values below
defaults:
filter: ["{ user_id: { eq: $user_id } }"]
# Fields and table names that you wish to block
blacklist:
- ar_internal_metadata
- schema_migrations
- secret
- password
- encrypted
- token
fields:
- name: users
# This filter will overwrite defaults.filter
filter: ["{ id: { eq: $user_id } }"]
- name: products
# Multiple filters are AND'd together
filter: [
"{ price: { gt: 0 } }",
"{ price: { lt: 8 } }"
]
- name: customers
# No filter is used for this field not
# even defaults.filter
filter: none
- # You can create new fields that have a
# real db table backing them
name: me
table: users
filter: ["{ id: { eq: $user_id } }"]
# - name: posts
# filter: ["{ account_id: { _eq: $account_id } }"]

View File

@ -3,6 +3,13 @@ services:
db: db:
image: postgres image: postgres
# nginx:
# image: nginx:latest
# volumes:
# - ./example/nginx.conf:/etc/nginx/nginx.conf
# ports:
# - 3001:3001
super_graph: super_graph:
build: build:
context: . context: .
@ -28,3 +35,4 @@ services:
depends_on: depends_on:
- db - db
- super_graph - super_graph
# - nginx

View File

@ -423,22 +423,33 @@ auth_fail_block: never
# sheep: sheep # sheep: sheep
auth: auth:
type: header # Can be 'rails' or 'jwt'
type: rails
cookie: _app_session cookie: _app_session
# Comment this out if you want to disable setting
# the user_id via a header. Good for testing
header: X-User-ID header: X-User-ID
# rails_cookie: rails:
# secret_key_base: caf335bfcfdb04e50db5bb0a4d67ab9... # Rails version this is used for reading the
# various cookies formats.
version: 5.2
# rails_memcache: # Found in 'Rails.application.config.secret_key_base'
# host: 127.0.0.1 secret_key_base: 0a248500a64c01184edb4d7ad3a805488f8097ac761b76aaa6c17c01dcb7af03a2f18ba61b2868134b9c7b79a122bc0dadff4367414a2d173297bfea92be5566
# rails_redis: # Remote cookie store. (memcache or redis)
# url: redis://127.0.0.1:6379 # url: redis://127.0.0.1:6379
# password: "" # password: test
# max_idle: 80, # max_idle: 80,
# max_active: 12000, # max_active: 12000,
# In most cases you don't need these
# salt: "encrypted cookie"
# sign_salt: "signed encrypted cookie"
# auth_salt: "authenticated encrypted cookie"
# jwt: # jwt:
# provider: auth0 # provider: auth0
# secret: abc335bfcfdb04e50db5bb0a4d67ab9 # secret: abc335bfcfdb04e50db5bb0a4d67ab9
@ -452,9 +463,9 @@ database:
dbname: app_development dbname: app_development
user: postgres user: postgres
password: '' password: ''
#pool_size: 10 # pool_size: 10
#max_retries: 0 # max_retries: 0
#log_level: "debug" # log_level: "debug"
# Define variables here that you want to use in filters # Define variables here that you want to use in filters
variables: variables:

View File

@ -8,7 +8,7 @@ Devise.setup do |config|
# confirmation, reset password and unlock tokens in the database. # confirmation, reset password and unlock tokens in the database.
# Devise will use the `secret_key_base` as its `secret_key` # Devise will use the `secret_key_base` as its `secret_key`
# by default. You can change it below and use your own secret key. # by default. You can change it below and use your own secret key.
# config.secret_key = 'da45f01cc03f113df2ac8788aab0833ff0ce6d3f2e298e964e0b35c7f63877588054a503106f0102d412987a45ed49e9a49697a6cf66da22f04ca6dafd24f76d' # config.secret_key = ''
# ==> Controller configuration # ==> Controller configuration
# Configure the parent class to the devise controllers. # Configure the parent class to the devise controllers.

View File

@ -0,0 +1 @@
Rails.application.config.secret_key_base= '0a248500a64c01184edb4d7ad3a805488f8097ac761b76aaa6c17c01dcb7af03a2f18ba61b2868134b9c7b79a122bc0dadff4367414a2d173297bfea92be5566'

25
example/nginx.conf Normal file
View File

@ -0,0 +1,25 @@
events { }
http {
server {
server_name localhost;
listen 3001;
location /api/v1/graphql {
proxy_pass http://super_graph:8080;
proxy_redirect off;
#rewrite ^/api/v1/graphql$ $1 break;
}
location /super-graph {
proxy_pass http://super_graph:8080;
proxy_redirect off;
rewrite ^/super-graph/(.*)$ /$1 break;
}
location / {
proxy_pass http://web:3000;
proxy_redirect off;
}
}
}

2
go.mod
View File

@ -1,6 +1,7 @@
module github.com/dosco/super-graph module github.com/dosco/super-graph
require ( require (
github.com/Masterminds/semver v1.4.2
github.com/adjust/gorails v0.0.0-20171013043634-2786ed0c03d3 github.com/adjust/gorails v0.0.0-20171013043634-2786ed0c03d3
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737 github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737
github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dgrijalva/jwt-go v3.2.0+incompatible
@ -13,6 +14,7 @@ require (
github.com/sirupsen/logrus v1.4.0 github.com/sirupsen/logrus v1.4.0
github.com/spf13/viper v1.3.1 github.com/spf13/viper v1.3.1
github.com/valyala/fasttemplate v1.0.1 github.com/valyala/fasttemplate v1.0.1
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a
mellium.im/sasl v0.2.1 // indirect mellium.im/sasl v0.2.1 // indirect
) )

2
go.sum
View File

@ -1,5 +1,7 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc=
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/adjust/gorails v0.0.0-20171013043634-2786ed0c03d3 h1:+qz9Ga6l6lKw6fgvk5RMV5HQznSLvI8Zxajwdj4FhFg= github.com/adjust/gorails v0.0.0-20171013043634-2786ed0c03d3 h1:+qz9Ga6l6lKw6fgvk5RMV5HQznSLvI8Zxajwdj4FhFg=
github.com/adjust/gorails v0.0.0-20171013043634-2786ed0c03d3/go.mod h1:FlkD11RtgMTYjVuBnb7cxoHmQGqvPpCsr2atC88nl/M= github.com/adjust/gorails v0.0.0-20171013043634-2786ed0c03d3/go.mod h1:FlkD11RtgMTYjVuBnb7cxoHmQGqvPpCsr2atC88nl/M=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=

View File

@ -1,79 +0,0 @@
title: Super Graph Production
host_port: 0.0.0.0:8080
web_ui: false
debug_level: 0
enable_tracing: false
# Throw a 401 on auth failure for queries that need auth
# valid values: always, per_query, never
auth_fail_block: always
# Postgres related environment Variables
# SG_DATABASE_HOST
# SG_DATABASE_PORT
# SG_DATABASE_USER
# SG_DATABASE_PASSWORD
# Auth related environment Variables
# SG_AUTH_RAILS_COOKIE_SECRET_KEY_BASE
# SG_AUTH_RAILS_REDIS_URL
# SG_AUTH_RAILS_REDIS_PASSWORD
# SG_AUTH_JWT_PUBLIC_KEY_FILE
# inflections:
# person: people
# sheep: sheep
auth:
type: cookie
cookie: _app_session
rails_cookie:
secret_key_base: caf335bfcfdb04e50db5bb0a4d67ab9...
# rails_memcache:
# host: 127.0.0.1
# rails_redis:
# url: redis://127.0.0.1:6379
# password: ""
# max_idle: 80,
# max_active: 12000,
# jwt:
# provider: auth0
# secret: abc335bfcfdb04e50db5bb0a4d67ab9
# public_key_file: /secrets/public_key.pem
# public_key_type: ecdsa #rsa
database:
type: postgres
host: db
port: 5432
dbname: app_development
user: postgres
password: ''
#pool_size: 10
#max_retries: 0
#log_level: "debug"
# Define variables here that you want to use in filters
variables:
account_id: "select account_id from users where id = $user_id"
# Define defaults to for the field key and values below
defaults:
filter: ["{ id: { _eq: $user_id } }"]
# Fields and table names that you wish to block
blacklist:
- ar_internal_metadata
- schema_migrations
- secret
- password
- encrypted
- token
fields:
- name: users
filter: ["{ id: { _eq: $user_id } }"]

171
rails/auth.go Normal file
View File

@ -0,0 +1,171 @@
package rails
import (
"encoding/json"
"errors"
"fmt"
"github.com/Masterminds/semver"
"github.com/adjust/gorails/marshal"
)
const (
salt = "encrypted cookie"
signSalt = "signed encrypted cookie"
authSalt = "authenticated encrypted cookie"
railsCipher = "aes-256-cbc"
railsCipher52 = "aes-256-gcm"
)
var (
errSessionData = errors.New("error decoding session data")
)
type Auth struct {
Cipher string
Secret string
Salt string
SignSalt string
AuthSalt string
}
func NewAuth(version, secret string) (*Auth, error) {
ra := &Auth{
Secret: secret,
Salt: salt,
SignSalt: signSalt,
AuthSalt: authSalt,
}
ver, err := semver.NewVersion(version)
if err != nil {
return nil, fmt.Errorf("rails auth: %s", err)
}
gt52, err := semver.NewConstraint(">= 5.2")
if err != nil {
return nil, fmt.Errorf("rails auth: %s", err)
}
if gt52.Check(ver) {
ra.Cipher = railsCipher52
} else {
ra.Cipher = railsCipher
}
return ra, nil
}
func (ra *Auth) ParseCookie(cookie string) (userID string, err error) {
var dcookie []byte
switch ra.Cipher {
case railsCipher:
dcookie, err = parseCookie(cookie, ra.Secret, ra.Salt, ra.SignSalt)
case railsCipher52:
dcookie, err = parseCookie52(cookie, ra.Secret, ra.AuthSalt)
default:
err = fmt.Errorf("unknown rails cookie cipher '%s'", ra.Cipher)
}
if err != nil {
return
}
if dcookie[0] != '{' {
userID, err = getUserId4(dcookie)
} else {
userID, err = getUserId(dcookie)
}
return
}
func ParseCookie(cookie string) (string, error) {
if cookie[0] != '{' {
return getUserId4([]byte(cookie))
}
return getUserId([]byte(cookie))
}
func getUserId(data []byte) (userID string, err error) {
var sessionData map[string]interface{}
err = json.Unmarshal(data, &sessionData)
if err != nil {
return
}
userKey, ok := sessionData["warden.user.user.key"]
if !ok {
err = errors.New("key 'warden.user.user.key' not found in session data")
}
items, ok := userKey.([]interface{})
if !ok {
err = errSessionData
return
}
if len(items) != 2 {
err = errSessionData
return
}
uids, ok := items[0].([]interface{})
if !ok {
err = errSessionData
return
}
uid, ok := uids[0].(float64)
if !ok {
err = errSessionData
return
}
userID = fmt.Sprintf("%d", int64(uid))
return
}
func getUserId4(data []byte) (userID string, err error) {
sessionData, err := marshal.CreateMarshalledObject(data).GetAsMap()
if err != nil {
return
}
wardenData, ok := sessionData["warden.user.user.key"]
if !ok {
err = errSessionData
return
}
wardenUserKey, err := wardenData.GetAsArray()
if err != nil {
return
}
if len(wardenUserKey) < 1 {
err = errSessionData
return
}
userData, err := wardenUserKey[0].GetAsArray()
if err != nil {
return
}
if len(userData) < 1 {
err = errSessionData
return
}
uid, err := userData[0].GetAsInteger()
if err != nil {
return
}
userID = fmt.Sprintf("%d", uid)
return
}

View File

@ -1,15 +1,22 @@
package serv package rails
import ( import (
"testing" "testing"
) )
func TestRailsEncryptedSession(t *testing.T) { func TestRailsEncryptedSession1(t *testing.T) {
cookie := "dDdjMW5jYUNYaFpBT1BSdFgwQkk4ZWNlT214L1FnM0pyZzZ1d21nSnVTTm9zS0ljN000S1JmT3cxcTNtRld2Ny0tQUFBQUFBQUFBQUFBQUFBQUFBQUFBQT09--75d8323b0f0e41cf4d5aabee1b229b1be76b83b6" cookie := "dDdjMW5jYUNYaFpBT1BSdFgwQkk4ZWNlT214L1FnM0pyZzZ1d21nSnVTTm9zS0ljN000S1JmT3cxcTNtRld2Ny0tQUFBQUFBQUFBQUFBQUFBQUFBQUFBQT09--75d8323b0f0e41cf4d5aabee1b229b1be76b83b6"
secret := "development_secret" secret := "development_secret"
userID, err := railsAuth(cookie, secret) ra := Auth{
Cipher: railsCipher,
Secret: secret,
Salt: salt,
SignSalt: signSalt,
}
userID, err := ra.ParseCookie(cookie)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
return return
@ -20,6 +27,29 @@ func TestRailsEncryptedSession(t *testing.T) {
} }
} }
func TestRailsEncryptedSession52(t *testing.T) {
cookie :=
"fZy1lt%2FIuXh2cpQgy3wWjbvabh1AqJX%2Bt6qO4D95DOZIpDhMyK2HqPFeNoaBtrXCUa9%2BDQuvbs1GX6tuccEAp14QPLNhm0PPJS5U1pRHqPLWaqT%2BBPYP%2BY9bo677komm9CPuOCOqBKf7rv3%2F4ptLmVO7iefB%2FP2ZlkV1848Johv5q%2B5PGyMxII2BEQnBdS3Petw6lRu741Bquc8z9VofC3t4%2F%2BLxVz%2BvBbTg--VL0MorYITXB8Dj3W--0yr0sr6pRU%2FwlYMQ%2BpEifA%3D%3D"
secret := "0a248500a64c01184edb4d7ad3a805488f8097ac761b76aaa6c17c01dcb7af03a2f18ba61b2868134b9c7b79a122bc0dadff4367414a2d173297bfea92be5566"
ra := Auth{
Cipher: railsCipher52,
Secret: secret,
AuthSalt: authSalt,
}
userID, err := ra.ParseCookie(cookie)
if err != nil {
t.Error(err)
return
}
if userID != "2" {
t.Errorf("Expecting userID 2 got %s", userID)
}
}
func TestRailsJsonSession(t *testing.T) { func TestRailsJsonSession(t *testing.T) {
sessionData := `{"warden.user.user.key":[[1],"secret"]}` sessionData := `{"warden.user.user.key":[[1],"secret"]}`

62
rails/cookie.go Normal file
View File

@ -0,0 +1,62 @@
package rails
import (
"crypto/aes"
"crypto/cipher"
"crypto/sha1"
"encoding/base64"
"net/url"
"strings"
"github.com/adjust/gorails/session"
"golang.org/x/crypto/pbkdf2"
)
func parseCookie(cookie, secretKeyBase, salt, signSalt string) ([]byte, error) {
return session.DecryptSignedCookie(
cookie,
secretKeyBase,
salt,
signSalt)
}
// {"session_id":"a71d6ffcd4ed5572ea2097f569eb95ef","warden.user.user.key":[[2],"$2a$11$q9Br7m4wJxQvF11hAHvTZO"],"_csrf_token":"HsYgrD2YBaWAabOYceN0hluNRnGuz49XiplmMPt43aY="}
func parseCookie52(cookie, secretKeyBase, authSalt string) ([]byte, error) {
ecookie, err := url.QueryUnescape(cookie)
if err != nil {
return nil, err
}
vectors := strings.Split(ecookie, "--")
body, err := base64.RawStdEncoding.DecodeString(vectors[0])
if err != nil {
return nil, err
}
iv, err := base64.RawStdEncoding.DecodeString(vectors[1])
if err != nil {
return nil, err
}
tag, err := base64.StdEncoding.DecodeString(vectors[2])
if err != nil {
return nil, err
}
key := pbkdf2.Key([]byte(secretKeyBase), []byte(authSalt),
1000, 32, sha1.New)
c, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(c)
if err != nil {
return nil, err
}
return gcm.Open(nil, iv, append(body, tag...), nil)
}

View File

@ -2,62 +2,47 @@ package serv
import ( import (
"context" "context"
"errors"
"net/http" "net/http"
) "strings"
const (
salt = "encrypted cookie"
signSalt = "signed encrypted cookie"
emptySecret = ""
authHeader = "Authorization"
) )
var ( var (
userIDProviderKey = struct{}{} userIDProviderKey = struct{}{}
userIDKey = struct{}{} userIDKey = struct{}{}
errSessionData = errors.New("error decoding session data")
) )
func headerHandler(next http.HandlerFunc) http.HandlerFunc { func headerAuth(r *http.Request, c *config) *http.Request {
fn := conf.Auth.Header if len(c.Auth.Header) == 0 {
if len(fn) == 0 { return nil
panic(errors.New("no auth.header defined"))
}
return func(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get(fn)
if len(userID) == 0 {
next.ServeHTTP(w, r)
return
} }
userID := r.Header.Get(c.Auth.Header)
if len(userID) != 0 {
ctx := context.WithValue(r.Context(), userIDKey, userID) ctx := context.WithValue(r.Context(), userIDKey, userID)
next.ServeHTTP(w, r.WithContext(ctx)) return r.WithContext(ctx)
} }
return nil
} }
func withAuth(next http.HandlerFunc) http.HandlerFunc { func withAuth(next http.HandlerFunc) http.HandlerFunc {
at := conf.Auth.Type at := conf.Auth.Type
ru := conf.Auth.Rails.URL
switch at { switch at {
case "header": case "rails":
return headerHandler(next) if strings.HasPrefix(ru, "memcache:") {
case "rails_cookie":
return railsCookieHandler(next)
case "rails_memcache":
return railsMemcacheHandler(next) return railsMemcacheHandler(next)
}
case "rails_redis": if strings.HasPrefix(ru, "redis:") {
return railsRedisHandler(next) return railsRedisHandler(next)
}
return railsCookieHandler(next)
case "jwt": case "jwt":
return jwtHandler(next) return jwtHandler(next)
default:
return next
} }
return next return next

View File

@ -10,6 +10,7 @@ import (
) )
const ( const (
authHeader = "Authorization"
jwtBase int = iota jwtBase int = iota
jwtAuth0 jwtAuth0
) )
@ -57,6 +58,11 @@ func jwtHandler(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
var tok string var tok string
if rn := headerAuth(r, conf); rn != nil {
next.ServeHTTP(w, rn)
return
}
if len(cookie) != 0 { if len(cookie) != 0 {
ck, err := r.Cookie(cookie) ck, err := r.Cookie(cookie)
if err != nil { if err != nil {

View File

@ -2,14 +2,14 @@ package serv
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"log"
"net/http" "net/http"
"net/url"
"github.com/adjust/gorails/marshal"
"github.com/adjust/gorails/session"
"github.com/bradfitz/gomemcache/memcache" "github.com/bradfitz/gomemcache/memcache"
"github.com/dosco/super-graph/rails"
"github.com/garyburd/redigo/redis" "github.com/garyburd/redigo/redis"
) )
@ -19,21 +19,20 @@ func railsRedisHandler(next http.HandlerFunc) http.HandlerFunc {
panic(errors.New("no auth.cookie defined")) panic(errors.New("no auth.cookie defined"))
} }
authURL := conf.Auth.RailsRedis.URL if len(conf.Auth.Rails.URL) == 0 {
if len(authURL) == 0 { log.Fatal(errors.New("no auth.rails.url defined"))
panic(errors.New("no auth.rails_redis.url defined"))
} }
rp := &redis.Pool{ rp := &redis.Pool{
MaxIdle: conf.Auth.RailsRedis.MaxIdle, MaxIdle: conf.Auth.Rails.MaxIdle,
MaxActive: conf.Auth.RailsRedis.MaxActive, MaxActive: conf.Auth.Rails.MaxActive,
Dial: func() (redis.Conn, error) { Dial: func() (redis.Conn, error) {
c, err := redis.DialURL(authURL) c, err := redis.DialURL(conf.Auth.Rails.URL)
if err != nil { if err != nil {
panic(err) panic(err)
} }
pwd := conf.Auth.RailsRedis.Password pwd := conf.Auth.Rails.Password
if len(pwd) != 0 { if len(pwd) != 0 {
if _, err := c.Do("AUTH", pwd); err != nil { if _, err := c.Do("AUTH", pwd); err != nil {
panic(err) panic(err)
@ -44,6 +43,11 @@ func railsRedisHandler(next http.HandlerFunc) http.HandlerFunc {
} }
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if rn := headerAuth(r, conf); rn != nil {
next.ServeHTTP(w, rn)
return
}
ck, err := r.Cookie(cookie) ck, err := r.Cookie(cookie)
if err != nil { if err != nil {
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
@ -57,7 +61,7 @@ func railsRedisHandler(next http.HandlerFunc) http.HandlerFunc {
return return
} }
userID, err := railsAuth(string(sessionData), emptySecret) userID, err := rails.ParseCookie(string(sessionData))
if err != nil { if err != nil {
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
@ -74,14 +78,23 @@ func railsMemcacheHandler(next http.HandlerFunc) http.HandlerFunc {
panic(errors.New("no auth.cookie defined")) panic(errors.New("no auth.cookie defined"))
} }
host := conf.Auth.RailsMemcache.Host if len(conf.Auth.Rails.URL) == 0 {
if len(host) == 0 { log.Fatal(errors.New("no auth.rails.url defined"))
panic(errors.New("no auth.rails_memcache.host defined"))
} }
mc := memcache.New(host) rURL, err := url.Parse(conf.Auth.Rails.URL)
if err != nil {
log.Fatal(err)
}
mc := memcache.New(rURL.Host)
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if rn := headerAuth(r, conf); rn != nil {
next.ServeHTTP(w, rn)
return
}
ck, err := r.Cookie(cookie) ck, err := r.Cookie(cookie)
if err != nil { if err != nil {
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
@ -95,7 +108,7 @@ func railsMemcacheHandler(next http.HandlerFunc) http.HandlerFunc {
return return
} }
userID, err := railsAuth(string(item.Value), emptySecret) userID, err := rails.ParseCookie(string(item.Value))
if err != nil { if err != nil {
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
@ -112,11 +125,17 @@ func railsCookieHandler(next http.HandlerFunc) http.HandlerFunc {
panic(errors.New("no auth.cookie defined")) panic(errors.New("no auth.cookie defined"))
} }
secret := conf.Auth.RailsCookie.SecretKeyBase ra, err := railsAuth(conf)
if len(secret) == 0 { if err != nil {
panic(errors.New("no auth.rails_cookie.secret_key_base defined")) log.Fatal(err)
} }
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if rn := headerAuth(r, conf); rn != nil {
next.ServeHTTP(w, rn)
return
}
ck, err := r.Cookie(cookie) ck, err := r.Cookie(cookie)
if err != nil { if err != nil {
logger.Error(err) logger.Error(err)
@ -124,7 +143,7 @@ func railsCookieHandler(next http.HandlerFunc) http.HandlerFunc {
return return
} }
userID, err := railsAuth(ck.Value, secret) userID, err := ra.ParseCookie(ck.Value)
if err != nil { if err != nil {
logger.Error(err) logger.Error(err)
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
@ -136,98 +155,33 @@ func railsCookieHandler(next http.HandlerFunc) http.HandlerFunc {
} }
} }
func railsAuth(cookie, secret string) (userID string, err error) { func railsAuth(c *config) (*rails.Auth, error) {
var dcookie []byte secret := c.Auth.Rails.SecretKeyBase
if len(secret) == 0 {
return nil, errors.New("no auth.rails.secret_key_base defined")
}
dcookie, err = session.DecryptSignedCookie(cookie, secret, salt, signSalt) version := c.Auth.Rails.Version
if len(version) == 0 {
return nil, errors.New("no auth.rails.version defined")
}
ra, err := rails.NewAuth(version, secret)
if err != nil { if err != nil {
return return nil, err
} }
if dcookie[0] != '{' { if len(c.Auth.Rails.Salt) != 0 {
userID, err = getUserId4(dcookie) ra.Salt = c.Auth.Rails.Salt
} else {
userID, err = getUserId(dcookie)
} }
return if len(conf.Auth.Rails.SignSalt) != 0 {
} ra.SignSalt = c.Auth.Rails.SignSalt
}
func getUserId(data []byte) (userID string, err error) {
var sessionData map[string]interface{} if len(conf.Auth.Rails.AuthSalt) != 0 {
ra.AuthSalt = c.Auth.Rails.AuthSalt
err = json.Unmarshal(data, &sessionData) }
if err != nil {
return return ra, nil
}
userKey, ok := sessionData["warden.user.user.key"]
if !ok {
err = errors.New("key 'warden.user.user.key' not found in session data")
}
items, ok := userKey.([]interface{})
if !ok {
err = errSessionData
return
}
if len(items) != 2 {
err = errSessionData
return
}
uids, ok := items[0].([]interface{})
if !ok {
err = errSessionData
return
}
uid, ok := uids[0].(float64)
if !ok {
err = errSessionData
return
}
userID = fmt.Sprintf("%d", int64(uid))
return
}
func getUserId4(data []byte) (userID string, err error) {
sessionData, err := marshal.CreateMarshalledObject(data).GetAsMap()
if err != nil {
return
}
wardenData, ok := sessionData["warden.user.user.key"]
if !ok {
err = errSessionData
return
}
wardenUserKey, err := wardenData.GetAsArray()
if err != nil {
return
}
if len(wardenUserKey) < 1 {
err = errSessionData
return
}
userData, err := wardenUserKey[0].GetAsArray()
if err != nil {
return
}
if len(userData) < 1 {
err = errSessionData
return
}
uid, err := userData[0].GetAsInteger()
if err != nil {
return
}
userID = fmt.Sprintf("%d", uid)
return
} }

View File

@ -48,20 +48,17 @@ type config struct {
Cookie string Cookie string
Header string Header string
RailsCookie struct { Rails struct {
Version string
SecretKeyBase string `mapstructure:"secret_key_base"` SecretKeyBase string `mapstructure:"secret_key_base"`
} `mapstructure:"rails_cookie"`
RailsMemcache struct {
Host string
} `mapstructure:"rails_memcache"`
RailsRedis struct {
URL string URL string
Password string Password string
MaxIdle int `mapstructure:"max_idle"` MaxIdle int `mapstructure:"max_idle"`
MaxActive int `mapstructure:"max_active"` MaxActive int `mapstructure:"max_active"`
} `mapstructure:"rails_redis"` Salt string
SignSalt string `mapstructure:"sign_salt"`
AuthSalt string `mapstructure:"auth_salt"`
}
JWT struct { JWT struct {
Provider string Provider string