Definitely not exceptions. A failed login is hardly an "exceptional" case and it just a normal course of logic for the application. If you use an exception then you'll always have to wrap logging in with an exception handler for no other reason than to handle the case of a failed login. That seems like the very definition of using exceptions for logic flow, which isn't right.
If you need to return specific information (which isn't always necessary from a login function, but might be in your case), #4 seems reasonable. You could take it a step further and make it an object:
public class LoginResult
{
// an enum for the status
// a string for a more specific message
// a valid user object on successful login
// etc.
}
Or, depending on the logic for it, an immutable struct instead of a class. (Make sure the struct is immutable, mutable structs are just asking for trouble.) The point being that you can apply all sorts of logic and functionality on the result object itself, which seems to be the direction you're heading.