Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d0b103a
Merge branch 'main' of github.com:gofr-dev/gofr into release/v1.42.3
Umang01-hash Jul 9, 2025
b560a96
udpdate release version to v1.42.3
Umang01-hash Jul 9, 2025
4bcc782
Merge pull request #2047 from gofr-dev/release/v1.42.3
Umang01-hash Jul 9, 2025
4894ffb
feat: add LDAP authentication module with JWT issuance
mundele2004 Jul 17, 2025
57ecc90
test: add edge case tests for LDAP failures and update main.go after …
mundele2004 Jul 21, 2025
68a6d3f
Merge branch 'development' into feature/ldap-auth
Umang01-hash Jul 23, 2025
9578e40
chore: fix golangci-lint errors and clean code formatting
mundele2004 Jul 30, 2025
e324976
fix: resolved merge conflict in version.go
mundele2004 Jul 30, 2025
0077f56
Merge branch 'development' into feature/ldap-auth
mundele2004 Aug 1, 2025
466a46b
Backup before running gci
mundele2004 Aug 10, 2025
2ea5d3b
fix all linter issue
mundele2004 Aug 10, 2025
81699e5
Fix import formatting to comply with gci linter
mundele2004 Aug 10, 2025
73f8018
Merge branch 'development' into feature/ldap-auth
mundele2004 Aug 10, 2025
9d6318d
Fix gci import order in ldapauth package
mundele2004 Aug 12, 2025
b3e3610
Fix gci import order in ldapauth package
mundele2004 Aug 12, 2025
ec784f3
Merge branch 'development' into feature/ldap-auth
mundele2004 Aug 12, 2025
fed560a
Merge branch 'development' into feature/ldap-auth
Umang01-hash Aug 13, 2025
be40c86
Merge branch 'development' into feature/ldap-auth
coolwednesday Aug 18, 2025
ea7bfe2
Merge branch 'development' into feature/ldap-auth
Umang01-hash Aug 20, 2025
142eba5
Merge branch 'development' into feature/ldap-auth
Umang01-hash Sep 2, 2025
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
85 changes: 85 additions & 0 deletions internal/auth/ldapauth/ldapauth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package ldapauth

import (
// "crypto/tls"
"fmt"
"time"

"github.com/go-ldap/ldap/v3"
"github.com/golang-jwt/jwt/v4"
)

type Config struct {
Addr string
BaseDN string
BindUserDN string // optional
BindPassword string // optional
JWTSecret string
}

type dialerFunc func(addr string, opts ...ldap.DialOpt) (*ldap.Conn, error)


type Authenticator struct {
cfg Config
dialFn dialerFunc
}

func New(cfg Config) *Authenticator {
return &Authenticator{
cfg: cfg,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the config specific to the LDAP library being used ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Config struct here is not directly from the go-ldap/ldap/v3 library — it's a custom wrapper defined within this module to keep the Authenticator decoupled from external configs.

This design allows flexibility — if we change or extend the backend (e.g. add StartTLS, change port or baseDN logic), we just update our config struct without impacting the rest of the app.

// Config holds LDAP server connection details and base search parameters.
// It is application-defined and not tied to the go-ldap library.

type Config struct {
      Addr     string
      BaseDN   string
      BindDN   string
      BindPass string
}

dialFn: ldap.DialURL,
}
}

func (a *Authenticator) Authenticate(username, password string) (string, error) {
// l, err := ldap.DialURL("ldap://" + a.cfg.Addr)
l, err := a.dialFn("ldap://" + a.cfg.Addr)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you elaborate on what l is ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, was wondering what possible validations we could have for a.cfg.Addr ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l is the LDAP connection (*ldap.Conn) returned by the dialer.

We should validate a.cfg.Addr for non-empty and proper host:port format using net.SplitHostPort.


if err != nil {
return "", fmt.Errorf("failed to connect LDAP: %w", err)
}
defer l.Close()

// err = l.StartTLS(&tls.Config{InsecureSkipVerify: true})
// if err != nil {
// return "", fmt.Errorf("TLS error: %w", err)
// }

if a.cfg.BindUserDN != "" {
if err := l.Bind(a.cfg.BindUserDN, a.cfg.BindPassword); err != nil {
return "", fmt.Errorf("bind failed: %w", err)
}
}

searchReq := ldap.NewSearchRequest(
a.cfg.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(uid=%s)", ldap.EscapeFilter(username)),
[]string{"dn"},
nil,
)

sr, err := l.Search(searchReq)
if err != nil || len(sr.Entries) != 1 {
return "", fmt.Errorf("user not found")
}

userDN := sr.Entries[0].DN

if err := l.Bind(userDN, password); err != nil {
return "", fmt.Errorf("invalid credentials")
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": username,
"exp": time.Now().Add(1 * time.Hour).Unix(),
})

signed, err := token.SignedString([]byte(a.cfg.JWTSecret))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should preferably be decomposed into smaller, testable sections

if err != nil {
return "", fmt.Errorf("token signing error: %w", err)
}

return signed, nil
}
27 changes: 27 additions & 0 deletions internal/auth/ldapauth/ldapauth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package ldapauth

import (
"errors"
"testing"

"github.com/go-ldap/ldap/v3"
)

func TestAuthenticate_InvalidUser(t *testing.T) {
auth := &Authenticator{
cfg: Config{
Addr: "fakehost",
BaseDN: "dc=example,dc=com",
JWTSecret: "testsecret",
},
dialFn: func(addr string, opts ...ldap.DialOpt) (*ldap.Conn, error) {
return nil, errors.New("dial error")
},

}

_, err := auth.Authenticate("testuser", "testpass")
if err == nil {
t.Fatal("expected error for invalid dial")
}
}
57 changes: 57 additions & 0 deletions mainFile/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package main

import (
"encoding/json"
"fmt"
"log"
"net/http"

"ldap-auth-go/internal/ldapauth"
)

var authenticator *ldapauth.Authenticator

func main() {
config := ldapauth.Config{
Addr: "localhost:389", // change this to your LDAP server
BaseDN: "dc=example,dc=com", // adjust as per your directory
BindUserDN: "cn=admin,dc=example,dc=com", // service account (optional)
BindPassword: "admin", // service password
JWTSecret: "your-secret-key", // replace with strong key
}

authenticator = ldapauth.New(config)

http.HandleFunc("/login", loginHandler)
log.Println("Server running on http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}

func loginHandler(w http.ResponseWriter, r *http.Request) {
var creds struct {
Username string `json:"username"`
Password string `json:"password"`
}

if err := json.NewDecoder(r.Body).Decode(&creds); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}

token, err := authenticator.Authenticate(creds.Username, creds.Password)
if err != nil {
http.Error(w, fmt.Sprintf("Login failed: %v", err), http.StatusUnauthorized)
return
}

// json.NewEncoder(w).Encode(map[string]string{
// "token": token,
// })
if err := json.NewEncoder(w).Encode(map[string]string{
"token": token,
}); err != nil {
http.Error(w, "Failed to write response", http.StatusInternalServerError)
return
}

}
10 changes: 10 additions & 0 deletions mainFile/testuser.ldif
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
dn: ou=users,dc=example,dc=com
objectClass: organizationalUnit
ou: users

dn: uid=testuser,ou=users,dc=example,dc=com
objectClass: inetOrgPerson
uid: testuser
sn: User
cn: Test User
userPassword: testpass