1

I'm building a login system using OAuth to integrate with various social networks.

The flow is very simple, first, the user is redirected to their social network to grant permission to read the necessary information, and after that my application has access to the following user information:

auth_id, auth_provider, email, name, picture

example with Facebook:

auth_id = fb-12345,
auth_provider = facebook,
email = [email protected],
name = john donate,
picture = http://site.com/profile.jpeg

this is the current Login UseCase:

export default class Login {
  private userRepositoryInterface: UserRepositoryInterface
  private tokenGeneratorAdapter: TokenGeneratorAdapterInterface
  private clockAdapterInterface: ClockAdapterInterface

  constructor(
    userRepositoryInterface: UserRepositoryInterface,
    tokenGeneratorAdapter: TokenGeneratorAdapter,
    clockAdapterInterface: ClockAdapterInterface
  ) {
    this.userRepositoryInterface = userRepositoryInterface;
    this.tokenGeneratorAdapter = tokenGeneratorAdapter;
    this.clockAdapterInterface = clockAdapterInterface
  }

  public async login(loginInput: LoginInput): Promise<LoginOutput> {

    let user: User;

    user = await this.userRepositoryInterface.findByAuthProviderAndAuthProviderId(loginInput.authProvider, loginInput.authProviderId);
    if (user === null) {
      const userId = await this.userRepositoryInterface.nextIdentity();
      user = new User({
        id: userId,
        name: loginInput.name,
        authProvider: loginInput.authProvider,
        authProviderId: loginInput.authProviderId,
        email: loginInput.email,
        profilePicture: loginInput.profilePicture,
        createdAt: this.clockAdapterInterface.now()
      });
      await this.userRepositoryInterface.create(user)
    }

    const authenticationToken = await this.tokenGeneratorAdapter.generate({
      id: user.id,
      name: user.name
    });

    return new LoginOutput(authenticationToken);
  }
}

The problems:

The problem with the current approach is that the Login UseCase has two responsibilities, the first one is to check if the user exists in the database (which would be the login itself), and the second is to create the user if it doesn't exist (sign up), and this is due to the way the OAuth flow works, as there is no failure scenario, whenever the user logs in to their social network, I will have access to the necessary information to either login or sign-up.

and the third problem is that the Login input DTO has many parameters that will not always be used depending on the scenario of login or sign-up, for example:

If the user exists in the database and the login is successful, the name, email, and profile picture data won't be used, but they always need to be present in the Use Case.

none of this seems OK to me, Do you know a proper way to handle this use case?

2
  • 4
    Note that you define what responsibility is. If you decide that "landing" means login or automatic sign-up + login, then you are not breaking anything. If you decide both must be different things that happen at different moments then implement two uses cases. OAuth doesn't force you to one or another. You can redirect the user to a pre-filled form during the sign-up. It's rare, but not impossible.
    – Laiv
    Aug 9, 2021 at 9:08
  • I see, I imagined this scenario but it's not going to be a good UX IMO, thanks! Aug 10, 2021 at 0:25

5 Answers 5

1

The role of the interactor or use case object in clean architecture is to model the flow of logic for that specific use case, or as Uncle Bob puts it, "control the dance of the entities". That is a single responsibility regardless of how many steps are involved.

A responsibility in this context is typically defined as a "reason to change", and your use case class is only going to change if the actual flow of logic changes. You've already extracted details like the repository into separate interfaces, so a change to the database, for example, will not affect your use case at all.

1

Most people who complain about “two responsibilities” have severely misunderstood what is meant by “responsibility”. If you go to the gym, you can be sure the person who checks your membership is going to be the one signing up new customers.

That’s because the responsibility is: Make sure that once your code is done, either the user is signed in, whatever needs doing to sign him in, or the user isn’t signed in and cannot be made to sign in.

And you need to do everything that is needed, like password reset if the user can’t remember their password, handling connectivity problems, everything. And creating a new account if needed.

0

In short your problems derive from Clean Architecture itself and therefore can not be really solved in CA.

You are only scratching the surface of what problems you're heading into, I've done a lengthy article of why CA isn't a good idea in general. For example not using all "data" from a DTO is one thing, but if you think about it, you're using the data not where it actually is. This means the reasons to change the data is not where the data is. This may violate some interpretations of SRP, not to mention fundamental principles of object-orientation. And on a practical note wreak havoc on the maintainability of your code.

What would be the "proper" way? That depends. However, you can start with making the thing object-oriented first, then have a second look. You're current implementation is procedural. That is, you have the login() procedure, you inject other procedures into it, like persistence, time, etc. related procedures.

Instead of that, think about what (business-related) things are in your applications and what behavior they must expose to be able to express the functionality you want. Hide technical things as much as possible. Try to make everything (all objects and methods) relevant to your requirements.

In summary: You'll have to think and design completely differently, it is not an easy fix.

3
  • So the question is: is it really likely that the application will become a non-web-application? If not, why couldn’t an object know that it will be presented on the Web? Really? We could say the same for any other aspect of the application, like database abstraction, 3rd party service integration... Why building abstraction or boundaries at all? No offence Robert, but the post's author is giving a biased view of the CA.
    – Laiv
    Aug 9, 2021 at 10:50
  • @Laiv Exactly. We should say that for all other technical things as well. Why build abstraction or boundaries at all? Another really good question! Why? And if we build those things, should those be about technology or business? Should we let technology dictate our overall design? Shouldn't the design be reflecting our understanding of the business case at hand? Should we optimize for changing technical details, or to change business-related things? When you start answering those and realize the implications, it's like slipping down a rabbit-hole. :) Aug 9, 2021 at 12:41
  • thanks for the feedback, i can see your point about not being proper OOP. could you clarify this part? "For example not using all "data" from a DTO is one thing, but if you think about it, you're using the data not where it actually is. also, if you could provide some code example of the login too, it'll be really helpful to grasp better the idea Aug 10, 2021 at 0:30
0

You are logging in the user, part of that requires that the user be in the database, so if the user isn’t in the db they have to be added.

What would you expect the use case DeleteUserBecauseOfGdprRequest to do when there are dependent records? Leave them be and violate the law? Have the admin giving the command delete each records individually? No, the “single responsibility” is to delete everything relating to that user.

Don’t confuse single responsibility with “single action”.

0

Sorry for being a little late, and please consider my answer. Well for the 1st problem (multiple responsibilities) and like others pointed before, responsibility is different than functionality, meaning a class doing multiple checks doesn't necessarily mean it have multiple responsibilities. 2nd problem, IMO the login as a whole isn't business specified rather than application specific, consider like you don't have users permission will that make the app nonexistent? The Login must be located in the outer layer which serves as an entry point to your application. 3rd problem related to DTO, this one is simple just supply the DTO with default values example :

{ProviderId : user.providerId || "",
 Email : user.email || ""....}

and make the getUser and setUser usecases to consume that DTO which both can be used inside the Login routine in the infrastructure layer.

Edit: The login process isn’t application logic specific, the login process may be constructed using both GetUserUsecase and SetUserUsecase use cases compounded together to get along with the OAuth flow which gets or create a user if it doesn’t already exist.

1
  • As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.
    – Community Bot
    Jul 5, 2022 at 18:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.