С полного нуля. Пользователь вводит логин + пароль:
Пользователь входит через ВК
Не имеет особого значения, ваша задача полученную информацию замапать на ID пользователя в ВАШЕЙ базе, с учетом необходимых проверок. ВК будет считаться доверенной стороной, т.е. ему вы доверяете процедуру проверки аутентичности юзера. Если аутентификация на вашей стороне - то проверяете вы (проверяете пароль по соленому хешу в базе, ну или как-то еще). Если у вас "быстрый вход" по ВК (без регистрации), то нужно сделать еще авторегистрацию, если userid не найден по данным от ВК. Правда, тут нужно быть готовым, что юзер захочет слинковать свои аккаунты, если он уже зарегался по логину/паролю. Ну или хотя бы предупредить при входе по ВК, что он в базе не найден и будет создан новый акк, чтобы он не удивлялся потом.
Что писать в куки
идентификатор сессии. Как уже сказали, должен быть сложным для копирования (скопированный идентификатор сессии - украденная сессия), уникальным. Погуглите алгоритмы или воспользуйтесь стандартыми. Сессии хранить или стандартными средствами, или попробовать
редиску (там есть авто-expire, что приятно).
Если нужно, чтобы у пользователя был только одновременный доступ с одного устройства - как быть?
проверять наличие сессии для данного пользователя (по ID пользователя с этапа аутентификации). Если сессия уже есть - убивать ее, создавать новую (новое устройство успешно зайдет, старое - "разлогинится"). Юзеру правда ничего не помешает скопировать идентификатор сессии из кукисов на другой девайс, так что можете еще в сессию IP писать, или user-агента (поменялся - пересоздаем сессию).
А если с нескольких?
ничего дополнительно не проверять, допусть создание любого количества сессий.
Как на каждой странице организовать проверку авторизован пользователь или нет?
идем в хранилище сессий (стандартные механизмы PHP/Redis), запрашиваем/стартуем сессию по идентификатору, пришедшему в куках. Если такой сессии нет (устарела, либо никогда и не было, идентификатор юзер сам придумал) - авторизацию не выполняем. Если сессия есть - то в зависимости от того, что мы там храним, либо достаем USERID, и пробиваем по основной БД его права, либо достаем права из самой сессии, если мы их там закешировали. "Выдача" прав - есть процедура авторизации. Теперь в зависимости от набора прав - меняем логику внутри скрипта. В больших системах применяют понятия ролей и групп - это все можно погуглить. Вкратце - при авторизации определяется, в каких группах состоит пользователь. Затем, по множеству групп сопоставляются роли, которые "играет" пользователь в системе (администратор данных/пользователей, администратор бэкапа, главбух, менеджер и т.д. и т.п.). Роли фиксированы, и зависят от логики приложения - в зависимости от имеющихся у пользователя ролей меняется и поведение приложения.
Также сейчас можно встретить claim-based подход, особенно в asp.net - тоже погуглите.
Что хранить в сессии?
Как минимум - USERID, но можно еще и закэшировать информацию о его правах, чтобы не читать постоянно ее из основной базы. Плюс, тут же хранить данные, относящиеся к САМОЙ сессии, например тот же IP входа, время входа и т.д. Тут зависит от задачи.