And consider what so many people call a junction table - a table that represents the many-to-many relationship between two tables. I will usually model these as just the two foreign keys to the two tables in the relationship, and a primary key over the combination of those columns. Again, a PK with business meaning. And again, I see no advantages to be gained by adding a surrogate, only a few disadvantages.
I can think of a giant advantage--at least to my programs.
Suppose I have a such a junction table (I call it a link table) with a primary key on the two FKs as you describe. Now suppose the user in the front-end tries to change one of the values in an existing row and unwittingly tries to change one of the FK values to a value that would result in a duplicate row.
The database will not allow this of course. Here's the problem: If my front-end program does nothing but submit the user's choice to the database, then the user will get a nasty database error that she will not understand.
On the other hand, suppose I am a good developer and check her data entries before the row is allowed to go to the database. This way, I can see if an error would occur with her choices and give her a nice, understandable error message.
How do I write such a query without a surrogate key? I have to ask the database: "Look for a row in the database with FK values 'A' and 'B', but don't look in this row." I need to exclude "this" row, because I'm looking for a duplicate and I do not know exactly what changes the user has made to the row. I want to look at other rows to see if the user's choices for this row would create a duplicate. How would I define "this" row without a surrogate key?
You might suggest that I simply count rows that have FK values A and B. If the query returns a count of 1, then I know that another row out there already has the FK values of interest and the user's choice would create a duplicate. However, what if the count of 1 is for the current row? Unless I have the hassle of writing screen-specific extra code (instead of the more generic code I use now for these situations), I don't know which value(s) in the row the user changed, especially when there are one other fields in the link table (which I know was not exactly your example). All my code knows is that something about the record changed, but not what specific column - or even if the user changed a FK value to something else and then changed it back to the original value before submitting the row to the database. (It happens. Don't ask me to explain users.) In such a case, a simple counting query that does not exclude the current row could simply return the current row. The code would think it is a duplicate when in fact it is not.
In the end, the surrogate key creates an easy, fool-proof system to be able to know exactly which row the user is modifying. So, then I have an easy way of making sure the user is given proper error messages and only when needed. With that surrogate key in place, I also get the added benefit of having a single column key should the link table ever be a parent in a join relationship in the future. (It happened at least once.)
I used to listen to the DBAs who advocated against surrogate keys for "link" tables. In practice, it created a problem. I had to go back and add surrogate keys to several tables in order to create proper user-friendly applications in reasonable time frames. Now, I just add those surrogate primary keys up-front and avoid the headaches down the line.