SwitchWorkspace switches the current user's active workspace and issues new tokens.
(ctx context.Context, req *connect.Request[v1pb.SwitchWorkspaceRequest])
| 1152 | |
| 1153 | // SwitchWorkspace switches the current user's active workspace and issues new tokens. |
| 1154 | func (s *AuthService) SwitchWorkspace(ctx context.Context, req *connect.Request[v1pb.SwitchWorkspaceRequest]) (*connect.Response[v1pb.LoginResponse], error) { |
| 1155 | request := req.Msg |
| 1156 | if request.Workspace == "" { |
| 1157 | return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("workspace is required")) |
| 1158 | } |
| 1159 | |
| 1160 | workspaceID, err := common.GetWorkspaceID(request.Workspace) |
| 1161 | if err != nil { |
| 1162 | return nil, connect.NewError(connect.CodeInvalidArgument, errors.Wrap(err, "invalid workspace name")) |
| 1163 | } |
| 1164 | |
| 1165 | user, ok := GetUserFromContext(ctx) |
| 1166 | if !ok || user == nil { |
| 1167 | return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("user not found")) |
| 1168 | } |
| 1169 | if user.Type != storepb.PrincipalType_END_USER { |
| 1170 | return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("only end users can switch workspaces")) |
| 1171 | } |
| 1172 | |
| 1173 | // Reject OAuth2 tokens — they are bound to a specific workspace via the OAuth client |
| 1174 | // and must not be used to mint plain user tokens for other workspaces. |
| 1175 | accessTokenStr, _ := auth.GetTokenFromHeaders(req.Header()) |
| 1176 | if accessTokenStr != "" { |
| 1177 | tokenClaims, err := auth.ExtractClaimsFromExpiredToken(accessTokenStr, s.secret) |
| 1178 | if err == nil && slices.Contains(tokenClaims.Audience, auth.OAuth2AccessTokenAudience) { |
| 1179 | return nil, connect.NewError(connect.CodePermissionDenied, errors.New("OAuth2 tokens cannot be used to switch workspaces")) |
| 1180 | } |
| 1181 | } |
| 1182 | |
| 1183 | // Verify the user is a member of the target workspace. |
| 1184 | ws, err := s.store.FindWorkspace(ctx, &store.FindWorkspaceMessage{ |
| 1185 | WorkspaceID: &workspaceID, |
| 1186 | Email: user.Email, |
| 1187 | IncludeAllUser: !s.profile.SaaS, |
| 1188 | }) |
| 1189 | if err != nil { |
| 1190 | return nil, connect.NewError(connect.CodeInternal, errors.Wrap(err, "failed to find workspace")) |
| 1191 | } |
| 1192 | if ws == nil { |
| 1193 | return nil, connect.NewError(connect.CodePermissionDenied, errors.Errorf("not a member of workspace %q", workspaceID)) |
| 1194 | } |
| 1195 | |
| 1196 | // Validate the target workspace's sign-in policies. |
| 1197 | if user.MemberDeleted { |
| 1198 | return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("user has been deactivated")) |
| 1199 | } |
| 1200 | if err := validateEmailWithDomains(ctx, s.licenseService, s.store, workspaceID, user.Email, false); err != nil { |
| 1201 | return nil, err |
| 1202 | } |
| 1203 | |
| 1204 | // Check MFA requirement for the target workspace. |
| 1205 | mfaSecondStep := request.GetMfaTempToken() != "" |
| 1206 | if mfaSecondStep { |
| 1207 | // Check MFA lockout before verifying. |
| 1208 | if err := s.checkMFALockout(ctx, user.Email); err != nil { |
| 1209 | return nil, err |
| 1210 | } |
| 1211 | // Verify the MFA temp token and OTP/recovery code. |
nothing calls this directly
no test coverage detected