Cookies tend to look like this: "dbee4b1823e815a9916e997c214d025fd1523530699". In essence, your browser has a cookies table with two columns: site and cookie. It is the job of the server to generate a cookie for you when you log in. This cookie is sent to you in the header of the HTTP response (be it a HTML document or JSON or anything else). From that point onwards, the cookie acts as your representative: your passport to server-land.
Systems that require storing data on client and server are called "stateful". Cookies are a stateful authentication approach. Like the browser, the server essentially stores a cookies table, but with the columns: cookie and identity. Here, identity could mean a username, a user id, etc.
JSON web tokens (JWT) tend to look like this:
JWTs are more of a general technology than a browser-specific technology. They can be used in any type of server-client setup, not just browsers. It's up to the developer how they would like to store a JWT. The JWT in the example is colour-coded to show three distinct parts: A header, a payload, and a signature.
The payload is where the authentication information sits. For example, it may contain the information "name: sean". Unlike cookies, JWT is a stateless approach. Once the server creates a token, it doesn't store that token anywhere. It simply sends it out to the user in the wild. But if there's no table to validate against, what's stopping someone from writing their own token and sending it to the server granting them all access? The magic lies in the server's private key: Only the private key can create and validate tokens.
One benefit of JWT over cookies is its protection against cross-site request forgery (as long as cookies aren't used to store the JWT). For this reason, it's common to store a JWT in the newer HTML5 web storage feature rather than cookies. HTML5 web storage allows you to store data persistently, or only for as long as a window is open in your browser. Importantly, unlike cookies which tether to the header of every outbound request, HTML5 web storage doesn't behave this way. It's the responsibility of the developer to add a token from HTML5 web storage to an Authorization header, and to load tokens into HTML5 web storage initially. This tackles CSRF issues by design.
However, JWT can still be compromised by cross-site scripting (XSS), unlike some cookies. Cookies can be set in a response header (server-side) accompanied by the HttpOnly flag. This prevents all access to the cookie, except by the browser's logic itself, which is responsible for attaching the cookie to all subsequent requests. This, along with a CSRF token, is good defence against both XSS and CSRF. It's a sacrifice of elegantly stateless design for the benefit of all-round security.
I've heard a few developers describe the choice of an authentication approach as "Choosing your poison". JWT can be more scalable, and they allow you to forget about CSRF tokens. They are very appropriate in a RESTful API, where an exception to statelessness is often made. However, if you use them alone on your main frontend, without HttpOnly cookies, you better make sure your site is XSS-proof: no arbitrary JS evals, screen third-party JS libraries you come across in the npm forest, and of course, keep the JWT validity period short to avoid someone hijacking your session from a library computer you forgot to log out of 5 years ago; unlike cookies, things in HTML5 web storage have no expiry date.