Changeset 724 in code for trunk


Ignore:
Timestamp:
Nov 21, 2021, 3:10:54 PM (4 years ago)
Author:
contact
Message:

Add support for post-connection-registration upstream SASL auth

Once the downstream connection has logged in with their bouncer
credentials, allow them to issue more SASL auths which will be
redirected to the upstream network. This allows downstream clients
to provide UIs to login to transparently login to upstream networks.

Location:
trunk
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/downstream.go

    r723 r724  
    245245}
    246246
     247type downstreamSASL struct {
     248        server                       sasl.Server
     249        plainUsername, plainPassword string
     250}
     251
    247252type downstreamConn struct {
    248253        conn
     
    268273        supportedCaps   map[string]string
    269274        caps            map[string]bool
     275        sasl            *downstreamSASL
    270276
    271277        lastBatchRef uint64
    272278
    273279        monitored casemapMap
    274 
    275         saslServer sasl.Server
    276280}
    277281
     
    687691                }
    688692        case "AUTHENTICATE":
    689                 if !dc.caps["sasl"] {
    690                         return ircError{&irc.Message{
     693                credentials, err := dc.handleAuthenticateCommand(msg)
     694                if err != nil {
     695                        return err
     696                } else if credentials == nil {
     697                        break
     698                }
     699
     700                if err := dc.authenticate(ctx, credentials.plainUsername, credentials.plainPassword); err != nil {
     701                        dc.logger.Printf("SASL authentication error: %v", err)
     702                        dc.endSASL(&irc.Message{
    691703                                Prefix:  dc.srv.prefix(),
    692704                                Command: irc.ERR_SASLFAIL,
    693                                 Params:  []string{"*", "AUTHENTICATE requires the \"sasl\" capability to be enabled"},
    694                         }}
    695                 }
    696                 if len(msg.Params) == 0 {
    697                         return ircError{&irc.Message{
    698                                 Prefix:  dc.srv.prefix(),
    699                                 Command: irc.ERR_SASLFAIL,
    700                                 Params:  []string{"*", "Missing AUTHENTICATE argument"},
    701                         }}
    702                 }
    703 
    704                 var resp []byte
    705                 if msg.Params[0] == "*" {
    706                         dc.saslServer = nil
    707                         return ircError{&irc.Message{
    708                                 Prefix:  dc.srv.prefix(),
    709                                 Command: irc.ERR_SASLABORTED,
    710                                 Params:  []string{"*", "SASL authentication aborted"},
    711                         }}
    712                 } else if dc.saslServer == nil {
    713                         mech := strings.ToUpper(msg.Params[0])
    714                         switch mech {
    715                         case "PLAIN":
    716                                 dc.saslServer = sasl.NewPlainServer(sasl.PlainAuthenticator(func(identity, username, password string) error {
    717                                         // TODO: we can't use the command context here, because it
    718                                         // gets cancelled once the command handler returns. SASL
    719                                         // might take multiple AUTHENTICATE commands to complete.
    720                                         return dc.authenticate(context.TODO(), username, password)
    721                                 }))
    722                         default:
    723                                 return ircError{&irc.Message{
    724                                         Prefix:  dc.srv.prefix(),
    725                                         Command: irc.ERR_SASLFAIL,
    726                                         Params:  []string{"*", fmt.Sprintf("Unsupported SASL mechanism %q", mech)},
    727                                 }}
    728                         }
    729                 } else if msg.Params[0] == "+" {
    730                         resp = nil
    731                 } else {
    732                         // TODO: multi-line messages
    733                         var err error
    734                         resp, err = base64.StdEncoding.DecodeString(msg.Params[0])
    735                         if err != nil {
    736                                 dc.saslServer = nil
    737                                 return ircError{&irc.Message{
    738                                         Prefix:  dc.srv.prefix(),
    739                                         Command: irc.ERR_SASLFAIL,
    740                                         Params:  []string{"*", "Invalid base64-encoded response"},
    741                                 }}
    742                         }
    743                 }
    744 
    745                 challenge, done, err := dc.saslServer.Next(resp)
    746                 if err != nil {
    747                         dc.saslServer = nil
    748                         if ircErr, ok := err.(ircError); ok && ircErr.Message.Command == irc.ERR_PASSWDMISMATCH {
    749                                 return ircError{&irc.Message{
    750                                         Prefix:  dc.srv.prefix(),
    751                                         Command: irc.ERR_SASLFAIL,
    752                                         Params:  []string{"*", ircErr.Message.Params[1]},
    753                                 }}
    754                         }
    755                         dc.SendMessage(&irc.Message{
    756                                 Prefix:  dc.srv.prefix(),
    757                                 Command: irc.ERR_SASLFAIL,
    758                                 Params:  []string{"*", "SASL error"},
    759                         })
    760                         return fmt.Errorf("SASL authentication failed: %v", err)
    761                 } else if done {
    762                         dc.saslServer = nil
    763                         // Technically we should send RPL_LOGGEDIN here. However we use
    764                         // RPL_LOGGEDIN to mirror the upstream connection status. Let's see
    765                         // how many clients that breaks. See:
    766                         // https://github.com/ircv3/ircv3-specifications/pull/476
    767                         dc.SendMessage(&irc.Message{
    768                                 Prefix:  dc.srv.prefix(),
    769                                 Command: irc.RPL_SASLSUCCESS,
    770                                 Params:  []string{dc.nick, "SASL authentication successful"},
    771                         })
    772                 } else {
    773                         challengeStr := "+"
    774                         if len(challenge) > 0 {
    775                                 challengeStr = base64.StdEncoding.EncodeToString(challenge)
    776                         }
    777 
    778                         // TODO: multi-line messages
    779                         dc.SendMessage(&irc.Message{
    780                                 Prefix:  dc.srv.prefix(),
    781                                 Command: "AUTHENTICATE",
    782                                 Params:  []string{challengeStr},
    783                         })
    784                 }
     705                                Params:  []string{"Authentication failed"},
     706                        })
     707                        break
     708                }
     709
     710                // Technically we should send RPL_LOGGEDIN here. However we use
     711                // RPL_LOGGEDIN to mirror the upstream connection status. Let's
     712                // see how many clients that breaks. See:
     713                // https://github.com/ircv3/ircv3-specifications/pull/476
     714                dc.endSASL(nil)
    785715        case "BOUNCER":
    786716                var subcommand string
     
    952882}
    953883
     884func (dc *downstreamConn) handleAuthenticateCommand(msg *irc.Message) (result *downstreamSASL, err error) {
     885        defer func() {
     886                if err != nil {
     887                        dc.sasl = nil
     888                }
     889        }()
     890
     891        if !dc.caps["sasl"] {
     892                return nil, ircError{&irc.Message{
     893                        Prefix:  dc.srv.prefix(),
     894                        Command: irc.ERR_SASLFAIL,
     895                        Params:  []string{"*", "AUTHENTICATE requires the \"sasl\" capability to be enabled"},
     896                }}
     897        }
     898        if len(msg.Params) == 0 {
     899                return nil, ircError{&irc.Message{
     900                        Prefix:  dc.srv.prefix(),
     901                        Command: irc.ERR_SASLFAIL,
     902                        Params:  []string{"*", "Missing AUTHENTICATE argument"},
     903                }}
     904        }
     905        if msg.Params[0] == "*" {
     906                return nil, ircError{&irc.Message{
     907                        Prefix:  dc.srv.prefix(),
     908                        Command: irc.ERR_SASLABORTED,
     909                        Params:  []string{"*", "SASL authentication aborted"},
     910                }}
     911        }
     912
     913        var resp []byte
     914        if dc.sasl == nil {
     915                mech := strings.ToUpper(msg.Params[0])
     916                var server sasl.Server
     917                switch mech {
     918                case "PLAIN":
     919                        server = sasl.NewPlainServer(sasl.PlainAuthenticator(func(identity, username, password string) error {
     920                                dc.sasl.plainUsername = username
     921                                dc.sasl.plainPassword = password
     922                                return nil
     923                        }))
     924                default:
     925                        return nil, ircError{&irc.Message{
     926                                Prefix:  dc.srv.prefix(),
     927                                Command: irc.ERR_SASLFAIL,
     928                                Params:  []string{"*", fmt.Sprintf("Unsupported SASL mechanism %q", mech)},
     929                        }}
     930                }
     931
     932                dc.sasl = &downstreamSASL{server: server}
     933        } else {
     934                // TODO: multi-line messages
     935                if msg.Params[0] == "+" {
     936                        resp = nil
     937                } else if resp, err = base64.StdEncoding.DecodeString(msg.Params[0]); err != nil {
     938                        return nil, ircError{&irc.Message{
     939                                Prefix:  dc.srv.prefix(),
     940                                Command: irc.ERR_SASLFAIL,
     941                                Params:  []string{"*", "Invalid base64-encoded response"},
     942                        }}
     943                }
     944        }
     945
     946        challenge, done, err := dc.sasl.server.Next(resp)
     947        if err != nil {
     948                return nil, err
     949        } else if done {
     950                return dc.sasl, nil
     951        } else {
     952                challengeStr := "+"
     953                if len(challenge) > 0 {
     954                        challengeStr = base64.StdEncoding.EncodeToString(challenge)
     955                }
     956
     957                // TODO: multi-line messages
     958                dc.SendMessage(&irc.Message{
     959                        Prefix:  dc.srv.prefix(),
     960                        Command: "AUTHENTICATE",
     961                        Params:  []string{challengeStr},
     962                })
     963                return nil, nil
     964        }
     965}
     966
     967func (dc *downstreamConn) endSASL(msg *irc.Message) {
     968        if dc.sasl == nil {
     969                return
     970        }
     971
     972        dc.sasl = nil
     973
     974        if msg != nil {
     975                dc.SendMessage(msg)
     976        } else {
     977                dc.SendMessage(&irc.Message{
     978                        Prefix:  dc.srv.prefix(),
     979                        Command: irc.RPL_SASLSUCCESS,
     980                        Params:  []string{dc.nick, "SASL authentication successful"},
     981                })
     982        }
     983}
     984
    954985func (dc *downstreamConn) setSupportedCap(name, value string) {
    955986        prevValue, hasPrev := dc.supportedCaps[name]
     
    11421173        }
    11431174
    1144         if dc.saslServer != nil {
    1145                 dc.saslServer = nil
    1146                 dc.SendMessage(&irc.Message{
     1175        if dc.sasl != nil {
     1176                dc.endSASL(&irc.Message{
    11471177                        Prefix:  dc.srv.prefix(),
    11481178                        Command: irc.ERR_SASLABORTED,
     
    23312361                        Params:  []string{upstreamUser, upstreamChannel},
    23322362                })
     2363        case "AUTHENTICATE":
     2364                // Post-connection-registration AUTHENTICATE is unsupported in
     2365                // multi-upstream mode, or if the upstream doesn't support SASL
     2366                uc := dc.upstream()
     2367                if uc == nil || !uc.caps["sasl"] {
     2368                        return ircError{&irc.Message{
     2369                                Prefix:  dc.srv.prefix(),
     2370                                Command: irc.ERR_SASLFAIL,
     2371                                Params:  []string{dc.nick, "Upstream network authentication not supported"},
     2372                        }}
     2373                }
     2374
     2375                credentials, err := dc.handleAuthenticateCommand(msg)
     2376                if err != nil {
     2377                        return err
     2378                }
     2379
     2380                if credentials != nil {
     2381                        if uc.saslClient != nil {
     2382                                dc.endSASL(&irc.Message{
     2383                                        Prefix:  dc.srv.prefix(),
     2384                                        Command: irc.ERR_SASLFAIL,
     2385                                        Params:  []string{dc.nick, "Another authentication attempt is already in progress"},
     2386                                })
     2387                                return nil
     2388                        }
     2389
     2390                        uc.logger.Printf("starting post-registration SASL PLAIN authentication with username %q", credentials.plainUsername)
     2391                        uc.saslClient = sasl.NewPlainClient("", credentials.plainUsername, credentials.plainPassword)
     2392                        uc.enqueueCommand(dc, &irc.Message{
     2393                                Command: "AUTHENTICATE",
     2394                                Params:  []string{"PLAIN"},
     2395                        })
     2396                }
    23332397        case "MONITOR":
    23342398                // MONITOR is unsupported in multi-upstream mode
     
    27012765func (dc *downstreamConn) handleNickServPRIVMSG(ctx context.Context, uc *upstreamConn, text string) {
    27022766        username, password, ok := parseNickServCredentials(text, uc.nick)
    2703         if !ok {
    2704                 return
    2705         }
    2706 
    2707         // User may have e.g. EXTERNAL mechanism configured. We do not want to
    2708         // automatically erase the key pair or any other credentials.
    2709         if uc.network.SASL.Mechanism != "" && uc.network.SASL.Mechanism != "PLAIN" {
    2710                 return
    2711         }
    2712 
    2713         dc.logger.Printf("auto-saving NickServ credentials with username %q", username)
    2714         n := uc.network
    2715         n.SASL.Mechanism = "PLAIN"
    2716         n.SASL.Plain.Username = username
    2717         n.SASL.Plain.Password = password
    2718         if err := dc.srv.db.StoreNetwork(ctx, dc.user.ID, &n.Network); err != nil {
    2719                 dc.logger.Printf("failed to save NickServ credentials: %v", err)
     2767        if ok {
     2768                uc.network.autoSaveSASLPlain(ctx, username, password)
    27202769        }
    27212770}
  • trunk/upstream.go

    r722 r724  
    3232        "message-tags":     true,
    3333        "multi-prefix":     true,
     34        "sasl":             true,
    3435        "server-time":      true,
    3536        "setname":          true,
     
    294295                                        Params:  []string{dc.nick, mask, "End of /WHO"},
    295296                                })
     297                        case "AUTHENTICATE":
     298                                dc.endSASL(&irc.Message{
     299                                        Prefix:  dc.srv.prefix(),
     300                                        Command: irc.ERR_SASLABORTED,
     301                                        Params:  []string{dc.nick, "SASL authentication aborted"},
     302                                })
    296303                        default:
    297304                                panic(fmt.Errorf("Unsupported pending command %q", pendingCmd.msg.Command))
     
    312319func (uc *upstreamConn) enqueueCommand(dc *downstreamConn, msg *irc.Message) {
    313320        switch msg.Command {
    314         case "LIST", "WHO":
     321        case "LIST", "WHO", "AUTHENTICATE":
    315322                // Supported
    316323        default:
     
    613620                uc.saslStarted = false
    614621
    615                 uc.SendMessage(&irc.Message{
    616                         Command: "CAP",
    617                         Params:  []string{"END"},
    618                 })
     622                if dc, _ := uc.dequeueCommand("AUTHENTICATE"); dc != nil && dc.sasl != nil {
     623                        if msg.Command == irc.RPL_SASLSUCCESS {
     624                                uc.network.autoSaveSASLPlain(context.TODO(), dc.sasl.plainUsername, dc.sasl.plainPassword)
     625                        }
     626
     627                        dc.endSASL(msg)
     628                }
     629
     630                if !uc.registered {
     631                        uc.SendMessage(&irc.Message{
     632                                Command: "CAP",
     633                                Params:  []string{"END"},
     634                        })
     635                }
    619636        case irc.RPL_WELCOME:
    620637                uc.registered = true
     
    17051722        }
    17061723
    1707         if uc.requestSASL() && !uc.caps["sasl"] {
    1708                 requestCaps = append(requestCaps, "sasl")
    1709         }
    1710 
    17111724        if len(requestCaps) == 0 {
    17121725                return
     
    17501763        switch name {
    17511764        case "sasl":
     1765                if !uc.requestSASL() {
     1766                        return nil
     1767                }
    17521768                if !ok {
    17531769                        uc.logger.Printf("server refused to acknowledge the SASL capability")
  • trunk/user.go

    r722 r724  
    405405}
    406406
     407func (net *network) autoSaveSASLPlain(ctx context.Context, username, password string) {
     408        // User may have e.g. EXTERNAL mechanism configured. We do not want to
     409        // automatically erase the key pair or any other credentials.
     410        if net.SASL.Mechanism != "" && net.SASL.Mechanism != "PLAIN" {
     411                return
     412        }
     413
     414        net.logger.Printf("auto-saving SASL PLAIN credentials with username %q", username)
     415        net.SASL.Mechanism = "PLAIN"
     416        net.SASL.Plain.Username = username
     417        net.SASL.Plain.Password = password
     418        if err := net.user.srv.db.StoreNetwork(ctx, net.user.ID, &net.Network); err != nil {
     419                net.logger.Printf("failed to save SASL PLAIN credentials: %v", err)
     420        }
     421}
     422
    407423type user struct {
    408424        User
Note: See TracChangeset for help on using the changeset viewer.