| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746 |
- package foodgroup
- import (
- "bytes"
- "context"
- "encoding/xml"
- "errors"
- "fmt"
- "log/slog"
- "strconv"
- "strings"
- "time"
- "github.com/mk6i/open-oscar-server/config"
- "github.com/mk6i/open-oscar-server/state"
- "github.com/mk6i/open-oscar-server/wire"
- "github.com/google/uuid"
- )
- // MaxConcurrentLoginsPerUser is the maximum number of concurrent logins allowed
- // for a single user.
- const MaxConcurrentLoginsPerUser = 5
- // NewAuthService creates a new instance of AuthService.
- func NewAuthService(
- cfg config.Config,
- sessionManager SessionRegistry,
- sessionRetriever SessionRetriever,
- chatSessionRegistry ChatSessionRegistry,
- userManager UserManager,
- cookieBaker CookieBaker,
- chatMessageRelayer ChatMessageRelayer,
- accountManager AccountManager,
- bartItemManager BARTItemManager,
- feedbagManager FeedbagManager,
- classes wire.RateLimitClasses,
- createAccount state.CreateAccountFunc,
- logger *slog.Logger,
- ) *AuthService {
- return &AuthService{
- chatSessionRegistry: chatSessionRegistry,
- config: cfg,
- cookieBaker: cookieBaker,
- sessionManager: sessionManager,
- sessionRetriever: sessionRetriever,
- userManager: userManager,
- chatMessageRelayer: chatMessageRelayer,
- accountManager: accountManager,
- bartItemManager: bartItemManager,
- feedbagManager: feedbagManager,
- rateLimitClasses: classes,
- timeNow: time.Now,
- maxConcurrentLoginsPerUser: MaxConcurrentLoginsPerUser,
- createAccount: createAccount,
- logger: logger,
- }
- }
- // AuthService provides client login and session management services. It
- // supports both FLAP (AIM v1.0-v3.0) and BUCP (AIM v3.5-v5.9) authentication
- // modes.
- type AuthService struct {
- chatMessageRelayer ChatMessageRelayer
- chatSessionRegistry ChatSessionRegistry
- config config.Config
- cookieBaker CookieBaker
- logger *slog.Logger
- sessionManager SessionRegistry
- sessionRetriever SessionRetriever
- userManager UserManager
- accountManager AccountManager
- bartItemManager BARTItemManager
- feedbagManager FeedbagManager
- rateLimitClasses wire.RateLimitClasses
- timeNow func() time.Time
- maxConcurrentLoginsPerUser int
- createAccount state.CreateAccountFunc
- }
- // RegisterChatSession adds a user to a chat room. The authCookie param is an
- // opaque token returned by {{OServiceService.ServiceRequest}} that identifies
- // the user and chat room. It returns the session object registered in the
- // ChatSessionRegistry.
- // This method does not verify that the user and chat room exist because it
- // implicitly trusts the contents of the token signed by
- // {{OServiceService.ServiceRequest}}.
- func (s AuthService) RegisterChatSession(ctx context.Context, authCookie state.ServerCookie, sessCfg func(sess *state.Session)) (*state.SessionInstance, error) {
- sess, err := s.chatSessionRegistry.AddSession(ctx, authCookie.ChatCookie, authCookie.ScreenName, sessCfg)
- if err != nil {
- return nil, fmt.Errorf("AddSession: %w", err)
- }
- sess.Session().SetRateClasses(time.Now(), s.rateLimitClasses)
- return sess, err
- }
- func (s AuthService) CrackCookie(authCookie []byte) (state.ServerCookie, error) {
- c := state.ServerCookie{}
- buf, err := s.cookieBaker.Crack(authCookie)
- if err != nil {
- return c, err
- }
- if err := wire.UnmarshalBE(&c, bytes.NewBuffer(buf)); err != nil {
- return c, err
- }
- return c, nil
- }
- // RegisterBOSSession adds a new session to the session registry.
- func (s AuthService) RegisterBOSSession(ctx context.Context, authCookie state.ServerCookie, sessCfg func(sess *state.Session)) (*state.SessionInstance, error) {
- u, err := s.userManager.User(ctx, authCookie.ScreenName.IdentScreenName())
- if err != nil {
- return nil, fmt.Errorf("failed to retrieve user: %w", err)
- }
- if u == nil {
- return nil, fmt.Errorf("user not found")
- }
- ctx, cancel := context.WithTimeout(ctx, time.Second*5)
- defer cancel()
- flag := wire.MultiConnFlag(authCookie.MultiConnFlag)
- doMultiSess := flag == wire.MultiConnFlagsRecentClient
- cfg := func(sess *state.Session) {
- sess.SetSignonTime(time.Now())
- sess.SetRateClasses(time.Now(), s.rateLimitClasses)
- sess.SetMemberSince(time.Now())
- }
- sess, err := s.sessionManager.AddSession(ctx, u.DisplayScreenName, doMultiSess, sessCfg, cfg)
- if err != nil {
- return nil, fmt.Errorf("AddSession: %w", err)
- }
- // set the unconfirmed user info flag if this account is unconfirmed
- if confirmed, err := s.accountManager.ConfirmStatus(ctx, sess.IdentScreenName()); err != nil {
- return nil, fmt.Errorf("error setting unconfirmed user flag: %w", err)
- } else if !confirmed {
- sess.SetUserInfoFlag(wire.OServiceUserFlagUnconfirmed)
- }
- if u.IsBot {
- sess.SetUserInfoFlag(wire.OServiceUserFlagBot)
- }
- sess.SetKerberosAuth(authCookie.KerberosAuth == 1)
- // set string containing OSCAR client name and version
- sess.SetClientID(authCookie.ClientID)
- sess.Session().SetOfflineMsgCount(u.OfflineMsgCount)
- if _, alreadySet := sess.Session().BuddyIcon(); !alreadySet {
- bartID, err := s.bartItemManager.BuddyIconMetadata(ctx, sess.IdentScreenName())
- if err != nil {
- return nil, fmt.Errorf("BuddyIconMetadata: %w", err)
- }
- if bartID != nil {
- sess.Session().SetBuddyIcon(*bartID)
- }
- }
- // indicate whether the client supports/wants multiple concurrent sessions
- sess.SetMultiConnFlag(flag)
- if u.DisplayScreenName.IsUIN() {
- sess.SetUserInfoFlag(wire.OServiceUserFlagICQ)
- uin, err := strconv.Atoi(u.IdentScreenName.String())
- if err != nil {
- return nil, fmt.Errorf("error converting username to UIN: %w", err)
- }
- sess.Session().SetUIN(uint32(uin))
- }
- return sess, nil
- }
- // RetrieveBOSSession returns a user's existing session instance
- func (s AuthService) RetrieveBOSSession(ctx context.Context, serverCookie state.ServerCookie) (*state.SessionInstance, error) {
- u, err := s.userManager.User(ctx, serverCookie.ScreenName.IdentScreenName())
- if err != nil {
- return nil, fmt.Errorf("failed to retrieve user: %w", err)
- }
- if u == nil {
- return nil, fmt.Errorf("user not found")
- }
- sess := s.sessionRetriever.RetrieveSession(u.IdentScreenName)
- if sess == nil {
- return nil, nil
- }
- return sess.Instance(serverCookie.SessionNum), nil
- }
- // Signout removes this user's session.
- func (s AuthService) Signout(ctx context.Context, session *state.Session) {
- s.sessionManager.RemoveSession(session)
- }
- // SignoutChat removes user from chat room and notifies remaining participants
- // of their departure.
- func (s AuthService) SignoutChat(ctx context.Context, sess *state.Session) {
- alertUserLeft(ctx, sess, s.chatMessageRelayer)
- s.chatSessionRegistry.RemoveSession(sess)
- }
- // BUCPChallenge processes a BUCP authentication challenge request. It
- // retrieves the user's auth key based on the screen name provided in the
- // request. The client uses the auth key to salt the MD5 password hash provided
- // in the subsequent login request. If the account is valid, return
- // SNAC(0x17,0x07), otherwise return SNAC(0x17,0x03).
- func (s AuthService) BUCPChallenge(ctx context.Context, inBody wire.SNAC_0x17_0x06_BUCPChallengeRequest, newUUID func() uuid.UUID) (wire.SNACMessage, error) {
- screenName, exists := inBody.String(wire.LoginTLVTagsScreenName)
- if !exists {
- s.logger.Debug("BUCPChallenge: screen name TLV not found in request")
- return wire.SNACMessage{}, errors.New("screen name doesn't exist in tlv")
- }
- s.logger.Debug("BUCPChallenge: received challenge request",
- "screen_name", screenName,
- "is_uin", state.DisplayScreenName(screenName).IsUIN())
- var authKey string
- user, err := s.userManager.User(ctx, state.NewIdentScreenName(screenName))
- if err != nil {
- s.logger.Error("BUCPChallenge: user lookup failed", "screen_name", screenName, "err", err.Error())
- return wire.SNACMessage{}, err
- }
- switch {
- case user != nil:
- // user lookup succeeded
- authKey = user.AuthKey
- s.logger.Debug("BUCPChallenge: user found, returning auth key",
- "screen_name", screenName,
- "auth_key_len", len(authKey))
- case s.config.DisableAuth:
- // can't find user, generate stub auth key
- authKey = newUUID().String()
- s.logger.Debug("BUCPChallenge: user not found, auth disabled, generating stub auth key",
- "screen_name", screenName)
- default:
- // can't find user, return login error
- s.logger.Debug("BUCPChallenge: user not found, returning error",
- "screen_name", screenName,
- "error_code", wire.LoginErrInvalidUsernameOrPassword)
- return wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.BUCP,
- SubGroup: wire.BUCPLoginResponse,
- },
- Body: wire.SNAC_0x17_0x03_BUCPLoginResponse{
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: []wire.TLV{
- wire.NewTLVBE(wire.LoginTLVTagsErrorSubcode, wire.LoginErrInvalidUsernameOrPassword),
- },
- },
- },
- }, nil
- }
- return wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.BUCP,
- SubGroup: wire.BUCPChallengeResponse,
- },
- Body: wire.SNAC_0x17_0x07_BUCPChallengeResponse{
- AuthKey: authKey,
- },
- }, nil
- }
- // BUCPLogin processes a BUCP authentication request for AIM v3.5-v5.9. Upon
- // successful login, a session is created.
- // If login credentials are invalid and app config DisableAuth is true, a stub
- // user is created and login continues as normal. DisableAuth allows you to
- // skip the account creation procedure, which simplifies the login flow during
- // development.
- // If login is successful, the SNAC TLV list contains the BOS server address
- // (wire.LoginTLVTagsReconnectHere) and an authorization cookie
- // (wire.LoginTLVTagsAuthorizationCookie). Else, an error code is set
- // (wire.LoginTLVTagsErrorSubcode).
- func (s AuthService) BUCPLogin(ctx context.Context, inBody wire.SNAC_0x17_0x02_BUCPLoginRequest, advertisedHost string) (wire.SNACMessage, error) {
- block, err := s.login(ctx, inBody.TLVList, advertisedHost)
- if err != nil {
- return wire.SNACMessage{}, err
- }
- return wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.BUCP,
- SubGroup: wire.BUCPLoginResponse,
- },
- Body: wire.SNAC_0x17_0x03_BUCPLoginResponse{
- TLVRestBlock: block,
- },
- }, nil
- }
- // FLAPLogin processes a FLAP authentication request for AIM v1.0-v3.0. Upon
- // successful login, a session is created.
- // If login credentials are invalid and app config DisableAuth is true, a stub
- // user is created and login continues as normal. DisableAuth allows you to
- // skip the account creation procedure, which simplifies the login flow during
- // development.
- // If login is successful, the SNAC TLV list contains the BOS server address
- // (wire.LoginTLVTagsReconnectHere) and an authorization cookie
- // (wire.LoginTLVTagsAuthorizationCookie). Else, an error code is set
- // (wire.LoginTLVTagsErrorSubcode).
- func (s AuthService) FLAPLogin(ctx context.Context, inFrame wire.FLAPSignonFrame, advertisedHost string) (wire.TLVRestBlock, error) {
- return s.login(ctx, inFrame.TLVList, advertisedHost)
- }
- // KerberosLogin handles AIM-style Kerberos authentication for AIM 6.0+.
- // Credit for understanding the SNAC structure and values goes to this mailing
- // list attachment from 2007:
- //
- // https://web.archive.org/web/20100619063015/http://pidgin.im/pipermail/devel/attachments/20070906/e0069ff5/attachment-0001.txt
- //
- // Several values in the response are poorly understood but necessary for proper
- // processing on the client side.
- func (s AuthService) KerberosLogin(ctx context.Context, inBody wire.SNAC_0x050C_0x0002_KerberosLoginRequest, advertisedHost string) (wire.SNACMessage, error) {
- b, ok := inBody.TicketRequestMetadata.Bytes(wire.KerberosTLVTicketRequest)
- if !ok {
- return wire.SNACMessage{}, fmt.Errorf("ticket request metadata bytes is missing")
- }
- var info wire.KerberosLoginRequestTicket
- if err := wire.UnmarshalBE(&info, bytes.NewReader(b)); err != nil {
- return wire.SNACMessage{}, fmt.Errorf("ticket request metadata unmarshal: %w", err)
- }
- list := wire.TLVList{
- wire.NewTLVBE(wire.LoginTLVTagsScreenName, inBody.ClientPrincipal),
- wire.NewTLVBE(wire.LoginTLVTagsMultiConnFlags, wire.MultiConnFlagsRecentClient),
- }
- if info.Version >= 4 {
- list = append(list, wire.NewTLVBE(wire.LoginTLVTagsRoastedKerberosPassword, info.Password))
- } else {
- list = append(list, wire.NewTLVBE(wire.LoginTLVTagsPlaintextKerberosPassword, info.Password))
- }
- result, err := s.login(ctx, list, advertisedHost)
- if err != nil {
- return wire.SNACMessage{}, fmt.Errorf("login: %w", err)
- }
- cookie, loginOK := result.Bytes(wire.LoginTLVTagsAuthorizationCookie)
- if !loginOK {
- return wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.Kerberos,
- SubGroup: wire.KerberosKerberosLoginErrResponse,
- },
- Body: wire.SNAC_0x050C_0x0004_KerberosLoginErrResponse{
- KerbRequestID: inBody.RequestID,
- ScreenName: inBody.ClientPrincipal,
- ErrCode: wire.KerberosErrAuthFailure,
- Message: "Auth failure",
- },
- }, nil
- }
- return wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.Kerberos,
- SubGroup: wire.KerberosLoginSuccessResponse,
- },
- Body: wire.SNAC_0x050C_0x0003_KerberosLoginSuccessResponse{
- RequestID: inBody.RequestID,
- Epoch: uint32(s.timeNow().Unix()),
- ClientPrincipal: inBody.ClientPrincipal,
- ClientRealm: "AOL",
- Tickets: []wire.KerberosTicket{
- {
- PVNO: 5,
- EncTicket: []byte{},
- TicketRealm: "AOL",
- ServicePrincipal: "im/boss",
- ClientRealm: "AOL",
- ClientPrincipal: inBody.ClientPrincipal,
- AuthTime: uint32(s.timeNow().Unix()),
- StartTime: uint32(s.timeNow().Unix()),
- EndTime: uint32(s.timeNow().Add(24 * time.Hour).Unix()),
- Unknown4: 1610612736,
- Unknown5: 1073741824,
- ConnectionMetadata: wire.TLVBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.KerberosTLVBOSServerInfo, wire.KerberosBOSServerInfo{
- Unknown: 1,
- ConnectionInfo: wire.TLVBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.KerberosTLVHostname, advertisedHost),
- wire.NewTLVBE(wire.KerberosTLVCookie, cookie),
- wire.NewTLVBE(wire.KerberosTLVConnSettings, wire.KerberosConnUseSSL),
- },
- },
- }),
- },
- },
- },
- },
- },
- }, nil
- }
- // loginProperties represents the properties sent by the client at login.
- type loginProperties struct {
- clientID string
- isBUCPAuth bool
- isFLAPAuth bool
- isFLAPJavaAuth bool
- isKerberosPlaintextAuth bool
- isKerberosRoastedAuth bool
- isPlaintextAuth bool
- isTOCAuth bool
- multiConnFlag uint8
- passwordHash []byte
- plaintextPassword []byte
- roastedPass []byte
- screenName state.DisplayScreenName
- }
- // fromTLV creates an instance of loginProperties from a TLV list.
- func (l *loginProperties) fromTLV(list wire.TLVList) error {
- // extract screen name
- if screenName, found := list.String(wire.LoginTLVTagsScreenName); found {
- l.screenName = state.DisplayScreenName(screenName)
- } else {
- return errors.New("screen name doesn't exist in tlv")
- }
- // extract client name and version
- if clientID, found := list.String(wire.LoginTLVTagsClientIdentity); found {
- l.clientID = clientID
- }
- // get the password from the appropriate TLV. older clients have a
- // roasted password, newer clients have a hashed password. ICQ may omit
- // the password TLV when logging in without saved password.
- switch {
- case list.HasTag(wire.LoginTLVTagsPasswordHash):
- // extract password hash for BUCP login
- l.passwordHash, _ = list.Bytes(wire.LoginTLVTagsPasswordHash)
- l.isBUCPAuth = true
- case list.HasTag(wire.LoginTLVTagsRoastedPassword):
- // extract roasted password for FLAP login
- l.roastedPass, _ = list.Bytes(wire.LoginTLVTagsRoastedPassword)
- if strings.HasPrefix(l.clientID, "AOL Instant Messenger (TM) version") &&
- strings.Contains(l.clientID, "for Java") {
- l.isFLAPJavaAuth = true
- } else {
- l.isFLAPAuth = true
- }
- case list.HasTag(wire.LoginTLVTagsRoastedTOCPassword):
- // extract roasted password for TOC FLAP login
- l.roastedPass, _ = list.Bytes(wire.LoginTLVTagsRoastedTOCPassword)
- l.isTOCAuth = true
- case list.HasTag(wire.LoginTLVTagsPlaintextKerberosPassword):
- l.plaintextPassword, _ = list.Bytes(wire.LoginTLVTagsPlaintextKerberosPassword)
- l.isKerberosPlaintextAuth = true
- case list.HasTag(wire.LoginTLVTagsPlaintextPassword):
- l.plaintextPassword, _ = list.Bytes(wire.LoginTLVTagsPlaintextPassword)
- l.isPlaintextAuth = true
- case list.HasTag(wire.LoginTLVTagsRoastedKerberosPassword):
- l.roastedPass, _ = list.Bytes(wire.LoginTLVTagsRoastedKerberosPassword)
- l.isKerberosRoastedAuth = true
- default:
- l.isFLAPAuth = true
- }
- // does the client support multiple concurrent sessions?
- if multiConnFlags, found := list.Uint8(wire.LoginTLVTagsMultiConnFlags); found {
- l.multiConnFlag = multiConnFlags
- }
- return nil
- }
- // login validates a user's credentials and creates their session. it returns
- // metadata used in both BUCP and FLAP authentication responses.
- func (s AuthService) login(ctx context.Context, tlv wire.TLVList, advertisedHost string) (wire.TLVRestBlock, error) {
- props := loginProperties{}
- if err := props.fromTLV(tlv); err != nil {
- s.logger.Debug("login: failed to parse TLVs", "err", err.Error())
- return wire.TLVRestBlock{}, err
- }
- s.logger.Debug("login: parsed login properties",
- "screen_name", props.screenName,
- "client_id", props.clientID,
- "is_bucp", props.isBUCPAuth,
- "is_flap", props.isFLAPAuth,
- "is_flap_java", props.isFLAPJavaAuth,
- "is_toc", props.isTOCAuth,
- "is_kerberos_plaintext", props.isKerberosPlaintextAuth,
- "is_kerberos_roasted", props.isKerberosRoastedAuth,
- "is_plaintext", props.isPlaintextAuth,
- "password_hash_len", len(props.passwordHash),
- "roasted_pass_len", len(props.roastedPass))
- user, err := s.userManager.User(ctx, props.screenName.IdentScreenName())
- if err != nil {
- s.logger.Error("login: user lookup failed", "screen_name", props.screenName, "err", err.Error())
- return wire.TLVRestBlock{}, err
- }
- if user == nil {
- s.logger.Debug("login: user not found", "screen_name", props.screenName)
- // user not found
- if s.config.DisableAuth {
- // auth disabled, create the user
- s.logger.Debug("login: auth disabled, creating user", "screen_name", props.screenName)
- return s.createUser(ctx, props, advertisedHost)
- }
- // auth enabled, return separate login errors for ICQ and AIM
- loginErr := wire.LoginErrInvalidUsernameOrPassword
- if props.screenName.IsUIN() {
- loginErr = wire.LoginErrICQUserErr
- }
- s.logger.Debug("login: returning user not found error",
- "screen_name", props.screenName,
- "error_code", loginErr)
- return loginFailureResponse(props, loginErr), nil
- }
- s.logger.Debug("login: user found", "screen_name", props.screenName, "is_icq", user.IsICQ)
- // check if suspended status should prevent login
- if user.SuspendedStatus > 0x0 {
- s.logger.Debug("login: user suspended",
- "screen_name", props.screenName,
- "suspended_status", user.SuspendedStatus)
- return loginFailureResponse(props, user.SuspendedStatus), nil
- }
- if s.config.DisableAuth {
- // user exists, but don't validate
- s.logger.Debug("login: auth disabled, skipping password validation", "screen_name", props.screenName)
- return s.loginSuccessResponse(ctx, props, advertisedHost)
- }
- var loginOK bool
- var authMethod string
- switch {
- case props.isBUCPAuth:
- authMethod = "BUCP"
- loginOK = user.ValidateHash(props.passwordHash)
- case props.isFLAPAuth:
- authMethod = "FLAP"
- loginOK = user.ValidateRoastedPass(props.roastedPass)
- case props.isFLAPJavaAuth:
- authMethod = "FLAP_Java"
- loginOK = user.ValidateRoastedJavaPass(props.roastedPass)
- case props.isTOCAuth:
- authMethod = "TOC"
- loginOK = user.ValidateRoastedTOCPass(props.roastedPass)
- case props.isKerberosPlaintextAuth:
- authMethod = "Kerberos_Plaintext"
- loginOK = user.ValidatePlaintextPass(props.plaintextPassword)
- case props.isKerberosRoastedAuth:
- authMethod = "Kerberos_Roasted"
- loginOK = user.ValidateRoastedKerberosPass(props.roastedPass)
- case props.isPlaintextAuth:
- authMethod = "Plaintext"
- loginOK = user.ValidatePlaintextPass(props.plaintextPassword)
- }
- s.logger.Debug("login: password validation result",
- "screen_name", props.screenName,
- "auth_method", authMethod,
- "login_ok", loginOK)
- if !loginOK {
- s.logger.Debug("login: password validation failed",
- "screen_name", props.screenName,
- "auth_method", authMethod)
- return loginFailureResponse(props, wire.LoginErrInvalidPassword), nil
- }
- // limit concurrent logins per user
- if props.multiConnFlag == uint8(wire.MultiConnFlagsRecentClient) {
- sess := s.sessionRetriever.RetrieveSession(props.screenName.IdentScreenName())
- if sess != nil && sess.InstanceCount() >= s.maxConcurrentLoginsPerUser {
- s.logger.Debug("login: too many concurrent sessions",
- "screen_name", props.screenName,
- "instance_count", sess.InstanceCount())
- return loginFailureResponse(props, wire.LoginErrRateLimitExceeded), nil
- }
- }
- s.logger.Debug("login: login successful", "screen_name", props.screenName)
- return s.loginSuccessResponse(ctx, props, advertisedHost)
- }
- func (s AuthService) createUser(ctx context.Context, props loginProperties, advertisedHost string) (wire.TLVRestBlock, error) {
- err := s.createAccount(ctx, props.screenName, "welcome1")
- if err != nil {
- switch {
- case errors.Is(err, state.ErrAIMHandleInvalidFormat) || errors.Is(err, state.ErrAIMHandleLength):
- return loginFailureResponse(props, wire.LoginErrInvalidUsernameOrPassword), nil
- case errors.Is(err, state.ErrICQUINInvalidFormat):
- return loginFailureResponse(props, wire.LoginErrICQUserErr), nil
- default:
- return wire.TLVRestBlock{}, err
- }
- }
- return s.loginSuccessResponse(ctx, props, advertisedHost)
- }
- func (s AuthService) loginSuccessResponse(ctx context.Context, props loginProperties, advertisedHost string) (wire.TLVRestBlock, error) {
- loginCookie := state.ServerCookie{
- Service: wire.BOS,
- ScreenName: props.screenName,
- ClientID: props.clientID,
- MultiConnFlag: props.multiConnFlag,
- }
- if props.isKerberosPlaintextAuth || props.isKerberosRoastedAuth {
- loginCookie.KerberosAuth = 1
- }
- buf := &bytes.Buffer{}
- if err := wire.MarshalBE(loginCookie, buf); err != nil {
- return wire.TLVRestBlock{}, err
- }
- cookie, err := s.cookieBaker.Issue(buf.Bytes())
- if err != nil {
- return wire.TLVRestBlock{}, fmt.Errorf("failed to issue auth cookie: %w", err)
- }
- reconnectHost := advertisedHost
- sslState := wire.OServiceServiceResponseSSLStateNotUsed
- s.logger.Debug("loginSuccessResponse: returning login response",
- "screen_name", props.screenName,
- "reconnect_host", reconnectHost,
- "ssl_state", sslState)
- loginTLVTags := wire.TLVList{
- wire.NewTLVBE(wire.LoginTLVTagsScreenName, props.screenName),
- wire.NewTLVBE(wire.LoginTLVTagsReconnectHere, reconnectHost),
- wire.NewTLVBE(wire.LoginTLVTagsAuthorizationCookie, cookie),
- wire.NewTLVBE(wire.OServiceTLVTagsSSLState, sslState),
- }
- if err := s.addLinkedAccountsTLV(ctx, props.screenName, &loginTLVTags); err != nil {
- return wire.TLVRestBlock{}, err
- }
- return wire.TLVRestBlock{TLVList: loginTLVTags}, nil
- }
- // addLinkedAccountsTLV builds the linked accounts XML and appends the
- // corresponding TLV to tlvs. If linkedNames is empty, tlvs is not modified.
- func (s AuthService) addLinkedAccountsTLV(ctx context.Context, screenName state.DisplayScreenName, tlvs *wire.TLVList) error {
- items, err := s.feedbagManager.Feedbag(ctx, screenName.IdentScreenName())
- if err != nil {
- return fmt.Errorf("failed to get linked accounts: %w", err)
- }
- linkedNames := state.NewFeedbagList(items, nil).LinkedScreenNames()
- if len(linkedNames) == 0 {
- return nil
- }
- acctXML, err := buildLinkedAccountsXML(screenName.IdentScreenName(), linkedNames)
- if err != nil {
- return fmt.Errorf("failed to build linked accounts xml: %w", err)
- }
- *tlvs = append(*tlvs, wire.NewTLVBE(wire.OServiceTLVTagsLinkedAccounts, acctXML))
- return nil
- }
- func loginFailureResponse(props loginProperties, errCode uint16) wire.TLVRestBlock {
- return wire.TLVRestBlock{
- TLVList: []wire.TLV{
- wire.NewTLVBE(wire.LoginTLVTagsScreenName, props.screenName),
- wire.NewTLVBE(wire.LoginTLVTagsErrorSubcode, errCode),
- },
- }
- }
- // buildLinkedAccountsXML will return the XML doc expected for wire.OServiceTLVTagsLinkedAccounts
- // Example:
- // <SET SETID="1">
- //
- // <RESREC TYPE="PRIMARY-ACCOUNT" ID="1"><n>PrimaryName</n></RESREC>
- // <RESREC TYPE="LINKED-ACCOUNT" ID="2"><n>LinkedName1</n></RESREC>
- // <RESREC TYPE="LINKED-ACCOUNT" ID="3"><n>LinkedName2</n></RESREC>
- // <RESREC TYPE="LINKED-ACCOUNT" ID="4"><n>LinkedName3</n></RESREC>
- // <RESREC TYPE="LINKED-ACCOUNT" ID="5"><n>LinkedName4</n></RESREC>
- //
- // </SET>
- func buildLinkedAccountsXML(screenName state.IdentScreenName, linkedNames []state.IdentScreenName) (string, error) {
- // ResRec represents the <RESREC> element with its attributes and nested <n> tag
- type ResRec struct {
- Type string `xml:"TYPE,attr"`
- ID string `xml:"ID,attr"`
- Name string `xml:"n"`
- }
- // Set represents the root <SET> element
- type Set struct {
- XMLName xml.Name `xml:"SET"`
- SetID string `xml:"SETID,attr"`
- Records []ResRec `xml:"RESREC"`
- }
- data := Set{
- SetID: "1",
- Records: []ResRec{
- {Type: "PRIMARY-ACCOUNT", ID: "1", Name: screenName.String()},
- },
- }
- for i, name := range linkedNames {
- newRec := ResRec{
- Type: "LINKED-ACCOUNT",
- // Incrementing ID starting from 2 (since Primary is 1)
- ID: strconv.Itoa(i + 2),
- Name: name.String(),
- }
- data.Records = append(data.Records, newRec)
- }
- output, err := xml.Marshal(data)
- if err != nil {
- return "", err
- }
- return string(output), nil
- }
|