├── .gitignore ├── Makefile ├── README.eng.md ├── README.md ├── config.example.json ├── nginx-ldap-auth-daemon.go └── nginx.conf /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | config.json 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | hash:=$(shell git rev-parse --short HEAD) 2 | 3 | default: nginx-ldap-auth-daemon 4 | 5 | all: 6 | echo $(hash) 7 | mkdir -p build/$(hash) 8 | 9 | GOOS=windows GOARCH=amd64 go build -o build/$(hash)/nginx-ldap-auth-daemon-windows-x64-$(hash).exe 10 | GOOS=windows GOARCH=386 go build -o build/$(hash)/nginx-ldap-auth-daemon-windows-386-$(hash).exe 11 | GOOS=linux GOARCH=amd64 go build -o build/$(hash)/nginx-ldap-auth-daemon-linux-x64-$(hash) 12 | GOOS=linux GOARCH=386 go build -o build/$(hash)/nginx-ldap-auth-daemon-linux-386-$(hash) 13 | GOOS=darwin GOARCH=amd64 go build -o build/$(hash)/nginx-ldap-auth-daemon-darwin-x64-$(hash) 14 | 15 | nginx-ldap-auth-daemon: 16 | mkdir -p build/$(hash) 17 | go build -o build/nginx-ldap-auth-daemon 18 | -------------------------------------------------------------------------------- /README.eng.md: -------------------------------------------------------------------------------- 1 | ## IMPORTANT 2 | 3 | **the project is just a demo which is for learning. I never run it in production.** 4 | 5 | ## install 6 | 7 | - build from source code 8 | 9 | git clone git@github.com:childe/ldap-nginx-golang.git 10 | cd ldap-nginx-golang 11 | make 12 | 13 | - download runnable bin file 14 | 15 | download it here [https://github.com/childe/ldap-nginx-golang/releases/tag/201607](https://github.com/childe/ldap-nginx-golang/releases/tag/201607) 16 | 17 | 18 | ## usage 19 | 20 | 1. make your config.json 21 | 22 | refer to config.example.json. *ladp chapter below explains what the parameters mean.* 23 | 24 | 2. nginx config 25 | 26 | cp nginx.conf /etc/nginx 27 | nginx -s reload 28 | 29 | *nginx chapter explains what they mean.* 30 | 31 | 32 | 3. run it 33 | 34 | ./nginx-ldap-auth-daemon --config config.json 35 | 36 | 37 | ## mechanism 38 | 39 | ### nginx 40 | The project depends on auth_request nginx module , but the module is not install by default. You need compile nginx with `--with-http_auth_request_module` 41 | 42 | refer to [http://nginx.org/en/docs/http/ngx_http_auth_request_module.html](http://nginx.org/en/docs/http/ngx_http_auth_request_module.html) 43 | 44 | explanation: 45 | 46 | location / { 47 | auth_request /auth-proxy; 48 | 49 | proxy_pass http://backend/; 50 | } 51 | the config means all requests would firstly be redirected to /auth-proxy. The request will be denied if /auth-proxy return 401 or 403; request will go on to http://backend/ if /auth-proxy return 2xx; Any other response code returned by the subrequest is considered an error. 52 | 53 | ### ldap 54 | 55 | ldap auth check steps: 56 | 57 | 1. connect to ldapserver 58 | 2. bind(means login) ldapserver according to binddn & bindpw 59 | 3. replace username input by user to filter template , and use it to search in ldap 60 | 5. return true if username is found in ldap AND could bind the user with the password input by user. 61 | 62 | 63 | ## Thanks 64 | 65 | One blog in nginx.com (https://www.nginx.com/blog/nginx-plus-authenticate-users/) has already givin detail method and also [example code](https://github.com/nginxinc/nginx-ldap-auth) 66 | 67 | I just implement it with golang in a much simpler way (removed many direct steps) 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 重要 2 | 3 | 这只是一个Demo, 用来学习的, 我也没有在生产环境跑过. 4 | 5 | [english readme](https://github.com/childe/ldap-nginx-golang/blob/master/README.eng.md) 6 | 7 | ## 安装 8 | 9 | - 从编码编译 10 | 11 | git clone git@github.com:childe/ldap-nginx-golang.git 12 | cd ldap-nginx-golang 13 | make 14 | 15 | - 或者下载编译好的二进制文件 16 | 17 | 从[https://github.com/childe/ldap-nginx-golang/releases/tag/201607](https://github.com/childe/ldap-nginx-golang/releases/tag/201607)下载对应的版本 18 | 19 | ## 使用 20 | 21 | 1. 配置config.json 22 | 23 | 参考 config.example.json, 配置项参考后面的ldap验证原理 24 | 25 | 2. 配置nginx 26 | 27 | cp nginx.conf /etc/nginx 28 | nginx -s reload 29 | 30 | 配置可参考后面的nginx原理 31 | 32 | 3. 运行 33 | 34 | ./nginx-ldap-auth-daemon --config config.json 35 | 36 | 所有参数都可以在运行时指定, 会覆盖config.json里面的值, 如下: 37 | 38 | ./nginx-ldap-auth-daemon --host 0.0.0.0 --port 9000 --insecureSkipVerify true 39 | 40 | **useSSL和insecureSkipVerify两个参数只能在运行时指定, 不能写在config.json里面, 因为我不知道golang里面怎么处理bool类型的options参数的默认值, 没办法和config.json里面的值做合并. 用interface好像也可以做, 但太麻烦了.** 41 | 42 | ### 查看所有参数 43 | ./nginx-ldap-auth-daemon --help 44 | 45 | 46 | ## 原理 47 | 48 | ### nginx 49 | 50 | 依赖auth_request这个模块, 但这个模块默认是不安装的, 需要编译nginx的时候加上--with-http_auth_request_module这个参数. 51 | 52 | 官方文档在[http://nginx.org/en/docs/http/ngx_http_auth_request_module.html](http://nginx.org/en/docs/http/ngx_http_auth_request_module.html) 53 | 54 | 简单解释一下: 55 | 56 | location / { 57 | auth_request /auth-proxy; 58 | 59 | proxy_pass http://backend/; 60 | } 61 | 62 | 这个意思是说, 所有访问先转到/auth-proxy这里, /auth-proxy如果返回401或者403, 则访问被拒绝; 如果返回2xx, 访问允许,继续被nginx转到http://backend/; 返回其他值, 会被认为是个错误. 63 | 64 | ### ldap 65 | 66 | ldap的验证步骤为: 67 | 68 | 1. 连接ldapserver 69 | 2. 根据 binddn, bindpw Bind到ldapserver (相当于登陆吧) 70 | 3. 把用户填写的用户名代入到filter模板中, 去ldap搜索 71 | 4. 用搜索到的DN去bind, 成功即验证成功 72 | 73 | 74 | ### 处理流程 75 | 76 | 参见[http://ohmycat.me/nginx/2016/06/28/nginx-ldap.html](http://ohmycat.me/nginx/2016/06/28/nginx-ldap.html) 77 | 78 | ## 感谢 79 | 80 | nginx的[一篇官方博客](https://www.nginx.com/blog/nginx-plus-authenticate-users/)已经给出了非常详细的ldap认证办法, 并给出了[示例代码](https://github.com/nginxinc/nginx-ldap-auth) 81 | 82 | 我只是用golang实现了一下, 并做了精简. 83 | -------------------------------------------------------------------------------- /config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "ldapserver": "cn1.global.ctrip.com:696", 3 | "basedn": "DC=cn,DC=global,DC=corp,DC=com", 4 | "binddn": "CN=apiuser,OU=User,OU=OPS,OU=Sh,DC=cn,DC=global,DC=corp,DC=com", 5 | "bindpw": "123456", 6 | "filter": "(sAMAccountName=%s)", 7 | "host": "127.0.0.1", 8 | "port": 8080 9 | } 10 | -------------------------------------------------------------------------------- /nginx-ldap-auth-daemon.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "encoding/base64" 6 | "encoding/json" 7 | "flag" 8 | "fmt" 9 | "gopkg.in/ldap.v2" 10 | "io/ioutil" 11 | "log" 12 | "net/http" 13 | "strings" 14 | ) 15 | 16 | var options = &struct { 17 | configArg string 18 | host string 19 | port int 20 | ldapserver string 21 | basedn string 22 | binddn string 23 | bindpw string 24 | filter string 25 | useSSL bool 26 | insecureSkipVerify bool 27 | }{} 28 | 29 | func ladpAuth(username string, password string) bool { 30 | var l *ldap.Conn 31 | var err error 32 | 33 | if options.useSSL { 34 | l, err = ldap.DialTLS("tcp", options.ldapserver, &tls.Config{ServerName: strings.SplitN(options.ldapserver, ":", 2)[0], InsecureSkipVerify: options.insecureSkipVerify}) 35 | } else { 36 | l, err = ldap.Dial("tcp", options.ldapserver) 37 | } 38 | 39 | if err != nil { 40 | log.Printf("connecting ldap server failed: %s\n", err) 41 | return false 42 | } 43 | 44 | bindErr := l.Bind(options.binddn, options.bindpw) 45 | 46 | if bindErr != nil { 47 | log.Printf("bind failed: %s", bindErr) 48 | return false 49 | } 50 | 51 | filterString := fmt.Sprintf(options.filter, username) 52 | log.Println(filterString) 53 | 54 | searchRequest := ldap.NewSearchRequest( 55 | options.basedn, 56 | ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, 57 | filterString, 58 | nil, 59 | nil, 60 | ) 61 | 62 | searchResult, searchErr := l.Search(searchRequest) 63 | if searchErr != nil { 64 | log.Printf("search failed: %s\n", searchErr) 65 | return false 66 | } 67 | 68 | if len(searchResult.Entries) != 1 { 69 | log.Println("search result is not only one(more or less)") 70 | return false 71 | } 72 | 73 | userDN := searchResult.Entries[0].DN 74 | log.Println(userDN) 75 | 76 | err = l.Bind(userDN, password) 77 | if err != nil { 78 | log.Println(err) 79 | return false 80 | } else { 81 | return true 82 | } 83 | } 84 | 85 | func authHandler(w http.ResponseWriter, r *http.Request) { 86 | log.Println(r.URL.Path) 87 | authorization := r.Header.Get("Authorization") 88 | 89 | if authorization == "" { 90 | w.Header().Add("WWW-Authenticate", "Basic realm=\"\"") 91 | w.WriteHeader(401) 92 | //fmt.Fprintf(w, "") 93 | return 94 | } 95 | 96 | authorizationBytes, err := base64.StdEncoding.DecodeString(authorization[len("Basic "):]) 97 | 98 | if err != nil { 99 | log.Println(err) 100 | //w.Header().Add("WWW-Authenticate", "Basic realm=\"\"") 101 | w.WriteHeader(403) 102 | return 103 | } 104 | 105 | authorizationValue := string(authorizationBytes) 106 | 107 | userANDpw := strings.SplitN(authorizationValue, ":", 2) 108 | if len(userANDpw) != 2 { 109 | log.Println("Authenticate Value Format Error") 110 | w.Header().Add("WWW-Authenticate", "Basic realm=\"\"") 111 | w.WriteHeader(403) 112 | return 113 | } 114 | 115 | username := userANDpw[0] 116 | password := userANDpw[1] 117 | 118 | log.Printf("username: %s", username) 119 | 120 | if ladpAuth(username, password) { 121 | w.WriteHeader(200) 122 | } else { 123 | w.Header().Add("WWW-Authenticate", "Basic realm=\"\"") 124 | w.WriteHeader(403) 125 | log.Println("Auth Failed") 126 | } 127 | return 128 | } 129 | 130 | func init() { 131 | flag.StringVar(&options.configArg, "config", "", "path to ldap-auth-daemon configuration file") 132 | flag.StringVar(&options.host, "host", "127.0.0.1", "ip/host that bind to, default 127.0.0.1") 133 | flag.IntVar(&options.port, "port", 8080, "port that bind to, default 8080") 134 | flag.StringVar(&options.ldapserver, "ldapserver", "", "required. ldapserver") 135 | flag.StringVar(&options.basedn, "basedn", "", "required. basedn.") 136 | flag.StringVar(&options.binddn, "binddn", "", "required if search action need bind first") 137 | flag.StringVar(&options.bindpw, "bindpw", "", "required if search action need bind first") 138 | flag.StringVar(&options.filter, "filter", "", "required. filter template, such as (sAMAccountName=%s)") 139 | flag.BoolVar(&options.useSSL, "useSSL", true, "if use SSL. default true") 140 | flag.BoolVar(&options.insecureSkipVerify, "insecureSkipVerify", false, "if skip verity when ldap server cert is not insecure. default false") 141 | } 142 | 143 | func mergeConfigToOptions(config map[string]interface{}) { 144 | if options.ldapserver == "" { 145 | if value, ok := config["ldapserver"]; ok { 146 | options.ldapserver = value.(string) 147 | } else { 148 | flag.PrintDefaults() 149 | log.Fatal("ldapserver is required") 150 | } 151 | } 152 | 153 | if options.basedn == "" { 154 | if value, ok := config["basedn"]; ok { 155 | options.basedn = value.(string) 156 | } else { 157 | flag.PrintDefaults() 158 | log.Fatal("basedn is required") 159 | } 160 | } 161 | 162 | if options.filter == "" { 163 | if value, ok := config["filter"]; ok { 164 | options.filter = value.(string) 165 | } else { 166 | flag.PrintDefaults() 167 | log.Fatal("filter is required") 168 | } 169 | } 170 | 171 | if options.binddn == "" { 172 | if value, ok := config["binddn"]; ok { 173 | options.binddn = value.(string) 174 | } 175 | } 176 | if options.bindpw == "" { 177 | if value, ok := config["bindpw"]; ok { 178 | options.bindpw = value.(string) 179 | } 180 | } 181 | } 182 | 183 | func main() { 184 | flag.Parse() 185 | log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds) 186 | 187 | config := map[string]interface{}{} 188 | if options.configArg != "" { 189 | configValue, err := ioutil.ReadFile(options.configArg) 190 | if err != nil { 191 | log.Fatal(err) 192 | return 193 | } 194 | 195 | if err := json.Unmarshal(configValue, &config); err != nil { 196 | log.Fatal(err) 197 | } 198 | } 199 | mergeConfigToOptions(config) 200 | 201 | http.HandleFunc("/", authHandler) 202 | log.Println(fmt.Sprintf("%s:%d", options.host, options.port)) 203 | http.ListenAndServe(fmt.Sprintf("%s:%d", options.host, options.port), nil) 204 | } 205 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | error_log /var/log/nginx/error.log debug; 2 | 3 | events { } 4 | 5 | http { 6 | proxy_cache_path cache/ keys_zone=auth_cache:10m; 7 | 8 | # Change the IP address if the daemon is not running on the 9 | # same host as NGINX/NGINX Plus. 10 | # the real service , such as elasticsearch 11 | upstream backend { 12 | server 127.0.0.1:9200; 13 | } 14 | 15 | # NGINX/NGINX Plus listen on port 80 for requests that require 16 | # authentication. Change the port number as appropriate. 17 | server { 18 | listen 80; 19 | 20 | # Protected application 21 | location / { 22 | auth_request /auth-proxy; 23 | 24 | proxy_pass http://backend/; 25 | } 26 | 27 | location = /auth-proxy { 28 | internal; 29 | 30 | # The ldap-auth daemon listens on port 8080, as set 31 | # in nginx-ldap-daemon.go 32 | # Change the IP address if the daemon is not running on 33 | # the same host as NGINX/NGINX Plus. 34 | proxy_pass http://127.0.0.1:8080; 35 | 36 | proxy_pass_request_body off; 37 | proxy_set_header Content-Length ""; 38 | proxy_cache auth_cache; 39 | proxy_cache_valid 200 403 10m; 40 | 41 | # The following directive adds the cookie to the cache key 42 | proxy_cache_key "$http_authorization$cookie_nginxauth"; 43 | } 44 | } 45 | } 46 | --------------------------------------------------------------------------------